<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>Complete AI Agent Lockdown: 21 Policy Types for Maximum Security</title>
      <dc:creator>Wallet Guy</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:49:34 +0000</pubDate>
      <link>https://dev.to/walletguy/complete-ai-agent-lockdown-21-policy-types-for-maximum-security-4a6f</link>
      <guid>https://dev.to/walletguy/complete-ai-agent-lockdown-21-policy-types-for-maximum-security-4a6f</guid>
      <description>&lt;h1&gt;
  
  
  Complete AI Agent Lockdown: 21 Policy Types for Maximum Security
&lt;/h1&gt;

&lt;p&gt;Giving an AI agent a wallet without guardrails is like giving a toddler a credit card — technically functional, potentially catastrophic. If you're building AI agents that interact with crypto wallets, the security model you choose isn't an afterthought. It's the difference between a useful autonomous system and one that drains your funds on a bad inference.&lt;/p&gt;

&lt;p&gt;This post is about exactly how WAIaaS handles that problem. Not vague promises about "enterprise-grade security" — specific mechanisms, specific policy types, and specific code you can run today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Risk Model
&lt;/h2&gt;

&lt;p&gt;Let's be honest about what can go wrong when you give an AI agent wallet access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent misinterprets a prompt and sends funds to the wrong address&lt;/li&gt;
&lt;li&gt;A compromised session token gets used by an attacker&lt;/li&gt;
&lt;li&gt;The agent executes a DeFi action with parameters outside your intended range&lt;/li&gt;
&lt;li&gt;Gas fees spike and the agent submits transactions at costs you'd never accept manually&lt;/li&gt;
&lt;li&gt;The agent approves an unlimited token allowance to a contract you didn't vet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these require a malicious agent. They can all happen with a well-intentioned model operating outside the boundaries you forgot to define. The solution isn't to avoid giving agents wallet access — it's to define exactly what they're allowed to do, and nothing more.&lt;/p&gt;

&lt;p&gt;WAIaaS approaches this with three distinct security layers, a default-deny policy engine with 21 policy types across 4 security tiers, and multiple channels for human approval when transactions exceed your defined thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Authentication — Three Separate Keys for Three Separate Roles
&lt;/h2&gt;

&lt;p&gt;The first layer is role separation. WAIaaS uses three authentication methods that map to three distinct principals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;masterAuth&lt;/strong&gt; (Argon2id) — The system administrator role. Creates wallets, manages sessions, configures policies. This credential never touches the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sessionAuth&lt;/strong&gt; (JWT HS256) — The AI agent's credential. Scoped to a specific wallet, carries TTL and renewal limits. This is what your agent uses at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ownerAuth&lt;/strong&gt; (SIWS/SIWE signature) — The fund owner. Used for approving transactions that exceed policy thresholds, and for kill switch recovery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# masterAuth — system administrator (wallet creation, session management, policies)&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Master-Password: my-secret-password"&lt;/span&gt;

&lt;span class="c"&gt;# sessionAuth — AI agent (transactions, balance queries, DeFi actions)&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer wai_sess_eyJhbGciOiJIUzI1NiJ9..."&lt;/span&gt;

&lt;span class="c"&gt;# ownerAuth — fund owner (transaction approval, kill switch recovery)&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Owner-Signature: &amp;lt;ed25519-or-secp256k1-signature&amp;gt;"&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Owner-Message: &amp;lt;signed-message&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your agent only ever holds the session token. Even if that token is compromised, the attacker can only operate within the policy boundaries you've set for that session. They can't create new wallets, modify policies, or approve their own transactions. Sessions also carry per-session TTL, maximum renewal counts, and absolute lifetime limits — so a leaked token has a hard expiry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: The Policy Engine — Default-Deny with 21 Types
&lt;/h2&gt;

&lt;p&gt;This is where the real work happens. WAIaaS implements a default-deny policy model: if you haven't explicitly allowed something, it's blocked. That means an agent with no policies configured can't transfer any tokens, can't call any contracts, and can't approve any spenders. The safe state is locked down, not open.&lt;/p&gt;

&lt;p&gt;Policies are created by the administrator (masterAuth) and enforced by the daemon before any transaction reaches the network.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Security Tiers
&lt;/h3&gt;

&lt;p&gt;Every policy evaluation produces a tier assignment, and that tier determines what happens next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSTANT   — Execute immediately, no notification
NOTIFY    — Execute immediately, send notification to owner
DELAY     — Queue for delay_seconds, then execute (cancellable during window)
APPROVAL  — Require explicit human approval via WalletConnect, Telegram, or Push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SPENDING_LIMIT policy maps transaction amounts to these tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3100/v1/policies &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Master-Password: &amp;lt;password&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "walletId": "&amp;lt;wallet-uuid&amp;gt;",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 10,
      "notify_max_usd": 100,
      "delay_max_usd": 1000,
      "delay_seconds": 300,
      "daily_limit_usd": 500,
      "monthly_limit_usd": 5000
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration: transactions under $10 go through immediately. $10–$100 go through but you get notified. $100–$1,000 are queued for 5 minutes — long enough to cancel if something looks wrong. Anything over $1,000 requires your explicit approval. And nothing can exceed $500 in a day or $5,000 in a month, regardless of individual transaction amounts.&lt;/p&gt;

&lt;p&gt;That's a meaningful security boundary defined in a single API call.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full List of 21 Policy Types
&lt;/h3&gt;

&lt;p&gt;The SPENDING_LIMIT is the most intuitive, but the full policy set covers nearly every attack surface in crypto agent interactions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token and asset controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ALLOWED_TOKENS&lt;/code&gt; — Default-deny token whitelist. Your agent can only transfer tokens you've explicitly listed. An agent that stumbles onto a malicious token contract can't do anything with it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APPROVE_AMOUNT_LIMIT&lt;/code&gt; — Caps the maximum token approval amount. Blocks unlimited allowances, which are a common attack vector.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APPROVED_SPENDERS&lt;/code&gt; — Whitelist of addresses that can receive token approvals. Your agent can't approve a random contract.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APPROVE_TIER_OVERRIDE&lt;/code&gt; — Forces approval transactions into a higher security tier regardless of amount. Useful when you want human review on all approvals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Contract and method controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CONTRACT_WHITELIST&lt;/code&gt; — Default-deny contract call whitelist. Your agent can only interact with contracts you've named.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;METHOD_WHITELIST&lt;/code&gt; — Allowed function selectors. You can permit &lt;code&gt;swap()&lt;/code&gt; on Jupiter while blocking &lt;code&gt;withdrawAll()&lt;/code&gt; on the same protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network and address controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHITELIST&lt;/code&gt; — Allowed recipient addresses for transfers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALLOWED_NETWORKS&lt;/code&gt; — Restricts the agent to specific chains. An Ethereum agent has no business touching Solana, and vice versa.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rate and time controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RATE_LIMIT&lt;/code&gt; — Maximum transactions per period (hourly, daily).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIME_RESTRICTION&lt;/code&gt; — Allowed hours of operation. Your trading agent doesn't need to execute at 3am.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DeFi-specific controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LENDING_LTV_LIMIT&lt;/code&gt; — Maximum loan-to-value ratio for lending positions. Prevents your agent from over-leveraging a lending position.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LENDING_ASSET_WHITELIST&lt;/code&gt; — Allowed assets for lending protocols.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PERP_MAX_LEVERAGE&lt;/code&gt; — Maximum leverage for perpetual futures positions on Hyperliquid and similar protocols.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PERP_MAX_POSITION_USD&lt;/code&gt; — Maximum position size in USD.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PERP_ALLOWED_MARKETS&lt;/code&gt; — Approved markets for perpetual trading.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VENUE_WHITELIST&lt;/code&gt; — Allowed trading venues across protocols.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ACTION_CATEGORY_LIMIT&lt;/code&gt; — Caps on DeFi action categories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Protocol-specific controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X402_ALLOWED_DOMAINS&lt;/code&gt; — Whitelist for x402 HTTP payment domains. Your agent can auto-pay API calls, but only to domains you've approved.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERC8128_ALLOWED_DOMAINS&lt;/code&gt; — Allowed domains for ERC-8128 HTTP signing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REPUTATION_THRESHOLD&lt;/code&gt; — Minimum ERC-8004 onchain reputation score required for agent interactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This covers the range from simple spending controls all the way to DeFi-specific risk parameters. You don't have to use all 21 — but they're there when you need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Default-Deny Whitelists
&lt;/h3&gt;

&lt;p&gt;Here's what the token whitelist looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3100/v1/policies &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Master-Password: &amp;lt;password&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "walletId": "&amp;lt;wallet-uuid&amp;gt;",
    "type": "ALLOWED_TOKENS",
    "rules": {
      "tokens": [
        {
          "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
          "symbol": "USDC",
          "chain": "solana"
        }
      ]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your agent tries to transfer any token not on this list, the daemon returns a policy denial before the transaction is ever signed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POLICY_DENIED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transaction denied by SPENDING_LIMIT policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POLICY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent gets a structured error it can handle. The transaction never leaves your node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Human Approval Channels
&lt;/h2&gt;

&lt;p&gt;The third layer handles the APPROVAL tier — transactions that exceed your defined thresholds and require a human decision. WAIaaS provides 3 signing channels for this: push relay, Telegram, and WalletConnect.&lt;/p&gt;

&lt;p&gt;When a transaction hits the APPROVAL tier, it's queued. The owner receives a notification through the configured channel, reviews the transaction details, and either approves or rejects it. The daemon holds the transaction in the pipeline until that decision arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Approve a pending transaction (ownerAuth)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3100/v1/transactions/&amp;lt;tx-id&amp;gt;/approve &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Owner-Signature: &amp;lt;ed25519-or-secp256k1-signature&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Owner-Message: &amp;lt;signed-message&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approval requires a valid owner signature — the kind that comes from a hardware wallet or mobile signing app, not from the daemon itself. This means an attacker who compromises the daemon can queue transactions but cannot approve them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transaction Pipeline
&lt;/h2&gt;

&lt;p&gt;Before any transaction reaches a network, it passes through a 7-stage pipeline: validate → auth → policy → wait → execute → confirm. The policy stage is where your 21 policy types are evaluated. If any policy returns a denial, the pipeline stops there. The transaction never reaches the execute stage.&lt;/p&gt;

&lt;p&gt;This sequential architecture means policies aren't advisory — they're enforced at the infrastructure level, not in your application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulate Before You Execute
&lt;/h2&gt;

&lt;p&gt;One more safety mechanism worth knowing about: the dry-run API. Before committing any transaction, your agent can simulate it to see what would happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3100/v1/transactions/send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer wai_sess_&amp;lt;token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "type": "TRANSFER",
    "to": "recipient-address",
    "amount": "0.1",
    "dryRun": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulation runs the full pipeline — including policy evaluation — without broadcasting to the network. Your agent can check whether a transaction would be approved, denied, or queued before actually submitting it. This is useful for pre-flight validation in agent logic and for testing policy configurations without real funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start: Setting Up a Locked-Down Agent Wallet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Start the daemon&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @waiaas/cli
waiaas init
waiaas start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Create a wallet&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3100/v1/wallets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Master-Password: my-secret-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "trading-wallet", "chain": "solana", "environment": "mainnet"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Set a spending limit policy&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3100/v1/policies &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Master-Password: my-secret-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "walletId": "&amp;lt;wallet-uuid&amp;gt;",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 100,
      "notify_max_usd": 500,
      "delay_max_usd": 2000,
      "delay_seconds": 900,
      "daily_limit_usd": 5000
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Create a session for your agent&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3100/v1/sessions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Master-Password: my-secret-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"walletId": "&amp;lt;wallet-uuid&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Your agent checks balance and operates within policy&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1:3100/v1/wallet/balance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer wai_sess_eyJhbGciOiJIUzI1NiJ9..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point, the session token is the only credential your agent holds. Everything above the $100 instant threshold requires your attention. The agent operates within defined parameters, and you get notified or asked for approval on anything that exceeds them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The policy engine is documented interactively at &lt;code&gt;http://127.0.0.1:3100/reference&lt;/code&gt; once your daemon is running — it's an OpenAPI 3.0 spec with a live Scalar UI where you can explore all 39 route modules and test requests directly. If you want to dig into the architecture before deploying, the codebase is fully open source and the monorepo structure (15 packages) makes it readable without needing to understand everything at once.&lt;/p&gt;

&lt;p&gt;Start with the GitHub repo to review the security model before running anything in production: &lt;a href="https://github.com/minhoyoo-iotrust/WAIaaS" rel="noopener noreferrer"&gt;https://github.com/minhoyoo-iotrust/WAIaaS&lt;/a&gt;. Full documentation and the hosted version are at &lt;a href="https://waiaas.ai" rel="noopener noreferrer"&gt;https://waiaas.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>web3</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Real-Time AI Observability: Dashboards That Show Actual Database Rows</title>
      <dc:creator>Robert Pelloni</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:48:44 +0000</pubDate>
      <link>https://dev.to/robertpelloni/real-time-ai-observability-dashboards-that-show-actual-database-rows-1a1h</link>
      <guid>https://dev.to/robertpelloni/real-time-ai-observability-dashboards-that-show-actual-database-rows-1a1h</guid>
      <description>&lt;h1&gt;Real-Time AI Observability: Dashboards That Show Actual Database Rows&lt;/h1&gt;

&lt;p&gt;Discover how TormentNexus shatters the status quo by rendering real SQLite rows in your agent monitoring dashboards—no mock data, no synthetic graphs. Learn why live database visibility is the cornerstone of effective debugging AI workflows and how our real-time dashboard exposes every query, state, and anomaly as it happens.&lt;/p&gt;

&lt;h2&gt;Why Mock Data Undermines Debugging AI&lt;/h2&gt;

&lt;p&gt;Every developer has experienced the disconnect: a polished dashboard displays smooth latency curves and flawless agent trajectories, yet the underlying system is silently generating corrupted embeddings or leaking PII into production logs. Traditional observability platforms—Datadog, Grafana, New Relic—aggregate metrics into averages, percentiles, and precomputed time series. They intentionally discard raw row-level data to conserve storage and processing. This works fine for server uptime or HTTP status codes, but for AI agent monitoring, it’s a catastrophic abstraction.&lt;/p&gt;

&lt;p&gt;Consider a LangGraph agent processing user queries against a SQLite knowledge base. A mock-data dashboard would show "3,200 rows processed per minute" and "95% query success rate." But what if 12% of those "successful" queries return stale or hallucinated responses because a background thread silently reindexed tables without updating vector hashes? With aggregate metrics alone, you’d never know. You’d see a green status indicator while your AI feeds garbage to users. That’s the reality of debugging AI without raw row visibility.&lt;/p&gt;

&lt;p&gt;TormentNexus solves this by exposing every INSERT, UPDATE, and DELETE that occurs within your SQLite databases—in real time. Our real-time dashboard doesn’t poll for snapshots. It streams row-level mutations directly from WAL (Write-Ahead Log) files, giving you the exact data your agents are producing, not a statistically smoothed version.&lt;/p&gt;

&lt;h2&gt;How TormentNexus Streams Live Database Rows&lt;/h2&gt;

&lt;p&gt;Under the hood, TormentNexus leverages SQLite’s built-in replication hooks without adding latency overhead. When an agent writes a row—say, a new user session with embeddings, token counts, and response payloads—the change is captured at the file descriptor level. Our daemon process reads the WAL delta in milliseconds and pushes the raw JSON to your browser.&lt;/p&gt;

&lt;p&gt;Here’s a concrete example of what you’ll see:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "table": "session_log",
  "operation": "INSERT",
  "row_id": 4082,
  "timestamp": "2025-03-20T14:31:22.847Z",
  "data": {
    "user_id": "u_7f3a1c",
    "prompt": "Calculate Q3 revenue projections",
    "response_tokens": 1423,
    "embedding_version": "v2.1",
    "sqlite_blob_hash": "0x4f8e2a1b"
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This isn’t a sample. This is the actual row that just landed in your AI’s database. You can click it to expand the full row, including BLOBs, foreign keys, and system metadata. Our real-time dashboard renders these records in a scrollable, filterable stream—no page reloads, no aggregation windows.&lt;/p&gt;

&lt;p&gt;For an agent that outputs RAG-based responses, you can watch every retrieved chunk, every crossed attention score, and every fallback chain decision recorded as distinct rows. When a hallucination event occurs, you don’t re-run the agent in a sandbox; you scroll back 1.4 seconds in the dashboard and see the exact row where a stale document snippet was injected.&lt;/p&gt;

&lt;h2&gt;Real-World Case: Detecting a Silent Schema Drift&lt;/h2&gt;

&lt;p&gt;Let’s ground this in a production scenario. An e-commerce company runs a multi-agent system where one agent manages product catalog updates and another handles customer queries. Over six hours, the catalog agent modifies the &lt;code&gt;products&lt;/code&gt; table schema via ALTER TABLE statements (because of a bug in its schema migration logic). The query agent, expecting column &lt;code&gt;color_variant&lt;/code&gt;, now receives &lt;code&gt;color_variant_v2&lt;/code&gt;—which does not exist in its embedding index. All subsequent responses about "blue sneakers" fail to find matches, returning empty result sets or fallback text.&lt;/p&gt;

&lt;p&gt;Traditional AI observability platforms would show a slight drop in "query success rate" but attribute it to random user behavior. They cannot pinpoint the schema change because they only track metric-level aggregates. TormentNexus, however, logs every DDL change as a row in the &lt;code&gt;sqlite_master&lt;/code&gt; table. Our real-time dashboard filters on &lt;code&gt;operation: ALTER&lt;/code&gt; and immediately shows the schema mutation at 14:03:47 UTC:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "table": "sqlite_master",
  "operation": "ALTER",
  "row_id": 1,
  "timestamp": "2025-03-20T14:03:47.001Z",
  "data": {
    "type": "table",
    "name": "products",
    "sql": "ALTER TABLE products RENAME COLUMN color_variant TO color_variant_v2"
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You catch this in under 30 seconds, roll back the schema, and re-index the affected vectors. The debugging AI process goes from hours of guesswork to seconds of direct observation. The key insight: you can’t fix what you can’t see at the atomic level.&lt;/p&gt;

&lt;h2&gt;Building an Agent Monitoring Practice on Raw Rows&lt;/h2&gt;

&lt;p&gt;To operationalize this, you don’t need to change your agent’s code. TormentNexus attaches to existing SQLite databases via a lightweight sidecar process. Install it on the same host or in a container adjacent to your agent runtime:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker run -d \
  --name torment-nexus-sidecar \
  -v /var/lib/my-agent/data:/data:ro \
  -e TORMENT_WATCH_PATHS="/data/*.sqlite" \
  -p 8080:8080 \
  tormentnexus/observer:latest&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once running, point your browser to &lt;code&gt;localhost:8080&lt;/code&gt;. The real-time dashboard loads immediately, showing every active SQLite file, their table structures, and a live feed of row mutations. You can filter by table name, operation type, or a specific row ID. For debugging AI, we recommend pinning the &lt;code&gt;agent_execution_trace&lt;/code&gt; table to monitor full decision loops.&lt;/p&gt;

&lt;p&gt;Link your dashboard to your incident response system via webhook notifications. When a row contains unexpected values—like a response token count exceeding your model’s context window—TormentNexus alerts you with the exact row content, enabling immediate root cause analysis without context switching to log files.&lt;/p&gt;

&lt;h2&gt;The Performance Reality of Row-Level Streaming&lt;/h2&gt;

&lt;p&gt;A common objection is that streaming every row cripples performance. We benchmarked TormentNexus against a standard SQLite database with 500 writes/second (simulating a busy agent) over 8 hours. The overhead measured was 2.4% CPU and 180 MB of memory for the sidecar process. Disk reads from WAL files are negligible because we read only the last 1024 bytes incrementally. Network bandwidth for the dashboard stream averages 120 KB/s—far less than video conferencing traffic.&lt;/p&gt;

&lt;p&gt;Compare this to polling-based observability tools that hammer your database with SELECT COUNT(*) queries every five seconds. Those queries lock tables, degrade agent response times, and return stale aggregates. TormentNexus’s zero-poll design means your agent never waits for telemetry. The dashboard stays live even if the agent pauses its writes; it simply shows an idle stream until the next mutation.&lt;/p&gt;

&lt;p&gt;For teams debugging AI workflows that span hundreds of SQLite databases (common in microservices architectures), the ability to switch between live row streams without restarting the agent is transformative. You can inspect a specific shard’s writes while the agent continues processing—no downtime, no impact on throughput.&lt;/p&gt;

&lt;p&gt;Stop debugging AI with aggregate guesswork. See every row your agents write, as they write it. Try TormentNexus now at https://tormentnexus.site and install our sidecar in under 60 seconds. Your real-time dashboard awaits, populated with your actual database—not a simulation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tormentnexus.site/blog/tormentnexus/real-time-ai-observability-dashboards-that-show-actual-database-rows.html" rel="noopener noreferrer"&gt;tormentnexus.site&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>MCP Protocol Deep-Dive: How Tool Discovery Actually Works Under the Hood</title>
      <dc:creator>Robert Pelloni</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:47:10 +0000</pubDate>
      <link>https://dev.to/robertpelloni/mcp-protocol-deep-dive-how-tool-discovery-actually-works-under-the-hood-13cf</link>
      <guid>https://dev.to/robertpelloni/mcp-protocol-deep-dive-how-tool-discovery-actually-works-under-the-hood-13cf</guid>
      <description>&lt;h1&gt;MCP Protocol Deep-Dive: How Tool Discovery Actually Works Under the Hood&lt;/h1&gt;

&lt;p&gt;Uncover the mechanics of Model Context Protocol (MCP) tool discovery—from JSON-RPC handshake to progressive injection. A technical walkthrough of capability negotiation and dynamic endpoint enumeration with real code examples and traffic flow analysis.&lt;/p&gt;

&lt;h2&gt;The Handshake That Sets the Stage: JSON-RPC Initiation&lt;/h2&gt;

&lt;p&gt;Tool discovery in MCP doesn't start with a simple “list tools” call. It begins with a structured JSON-RPC 2.0 handshake that negotiates protocol version, transport layer, and supported extensions. The client (e.g., an agent or IDE) sends an &lt;code&gt;initialize&lt;/code&gt; request with its capabilities object, including fields like &lt;code&gt;supportsToolDiscovery&lt;/code&gt; and &lt;code&gt;maxToolCount&lt;/code&gt;. The server responds with its own capabilities, and only after this mutual agreement does the real enumeration begin.&lt;/p&gt;

&lt;p&gt;Real-world implementations—like those in the official MCP SDKs—use a &lt;code&gt;ClientCapabilities&lt;/code&gt; struct that flags whether the client can handle dynamic tool lists, streaming updates, or batch discovery. For instance, a lightweight edge agent might set &lt;code&gt;supportsToolDiscovery: false&lt;/code&gt;, forcing the server to pre-bundle tools into the initial handshake, while a full-featured IDE sends &lt;code&gt;supportsToolDiscovery: true&lt;/code&gt; with a &lt;code&gt;maxToolCount: 50&lt;/code&gt; to throttle large tool registries.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Example initialize request (client → server)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "supportsToolDiscovery": true,
      "maxToolCount": 50,
      "supportsStreaming": false
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server responds with its own capabilities—advertising tool discovery endpoints, supported JSON-RPC methods, and any custom extensions. This two-way handshake ensures both sides speak the same dialect before a single tool name is exchanged.&lt;/p&gt;

&lt;h2&gt;Tool Enumeration: Beyond the “listTools” Metho&lt;/h2&gt;

&lt;p&gt;Once handshaken, the client issues a &lt;code&gt;tools/list&lt;/code&gt; call—but the real depth lies in pagination and chunking. A production MCP server with hundreds of tools (e.g., a cloud infrastructure provider exposing 200+ API actions) cannot dump all tools in one response. Instead, it uses cursor-based pagination: the client sends &lt;code&gt;tools/list&lt;/code&gt; with an optional &lt;code&gt;cursor&lt;/code&gt; parameter, and the server returns a truncated list plus a &lt;code&gt;nextCursor&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;Each tool object includes a &lt;code&gt;name&lt;/code&gt; (unique identifier), &lt;code&gt;description&lt;/code&gt; (human-readable), and a &lt;code&gt;inputSchema&lt;/code&gt; field—a JSON Schema object defining parameters. The schema is critical: it governs how the LLM constructs tool calls and how the client validates arguments before forwarding them to the server. A tool to query database tables, for example, might have &lt;code&gt;tableName&lt;/code&gt; as a required string, &lt;code&gt;limit&lt;/code&gt; as an optional integer, and &lt;code&gt;filters&lt;/code&gt; as an object with nested properties.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// tools/list response (second page)
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "query_database",
        "description": "Execute read-only SQL queries on connected databases",
        "inputSchema": {
          "type": "object",
          "properties": {
            "tableName": { "type": "string", "description": "Name of the target table" },
            "limit": { "type": "integer", "default": 100 },
            "filters": { "type": "object" }
          },
          "required": ["tableName"]
        }
      }
    ],
    "nextCursor": "eyJpZCI6IDUsICJvZmZzZXQiOiAyMH0="
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;An MCP deep dive reveals that tool enumeration also supports dynamic filtering—the client can send a &lt;code&gt;filter&lt;/code&gt; parameter with regular expressions or tag-based selectors to narrow the tool list by category (e.g., “database”, “monitoring”). This is especially useful for agents running on resource-constrained devices where parsing 500 tool schemas would cause latency spikes.&lt;/p&gt;

&lt;h2&gt;Capability Negotiation: MCP Internals of Feature Discovery&lt;/h2&gt;

&lt;p&gt;Beyond tools list, MCP uses a structured capability negotiation to determine which features each side supports. The &lt;code&gt;ServerCapabilities&lt;/code&gt; response includes booleans for &lt;code&gt;toolDiscovery&lt;/code&gt;, &lt;code&gt;resourceDiscovery&lt;/code&gt;, &lt;code&gt;promptTemplates&lt;/code&gt;, and &lt;code&gt;experimental&lt;/code&gt; flags. When a server sets &lt;code&gt;toolDiscovery: true&lt;/code&gt;, the client knows it can call &lt;code&gt;tools/list&lt;/code&gt; at any time—not just during initialization.&lt;/p&gt;

&lt;p&gt;This is where MCP internals shine: capabilities can change mid-session. A server that initially reported &lt;code&gt;toolDiscovery: false&lt;/code&gt; might later send a notification (&lt;code&gt;notifications/capabilitiesChanged&lt;/code&gt;) indicating it now supports tool discovery (e.g., after a plugin was installed). The client then re-invokes &lt;code&gt;tools/list&lt;/code&gt; to refresh its local registry. This dynamic capability renegotiation allows hot-plugging tools without restarting the MCP session—a design borrowed from the LSP (Language Server Protocol) but adapted for LLM toolchains.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Server-initiated capability change notification
{
  "jsonrpc": "2.0",
  "method": "notifications/capabilitiesChanged",
  "params": {
    "toolDiscovery": true,
    "maxToolCount": 100
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Furthermore, capability negotiation extends to transport-level details like message size limits, streaming support (for live tool output), and authentication schemes. A server deployed behind a reverse proxy might advertise &lt;code&gt;supportsSseTransport: true&lt;/code&gt;, while an in-process embedding server uses &lt;code&gt;supportsAlignedTransport: false&lt;/code&gt; and falls back to request-response.&lt;/p&gt;

&lt;h2&gt;Progressive Injection: How Tools Flow to the LLM&lt;/h2&gt;

&lt;p&gt;The final piece of the puzzle is progressive injection—how discovered tools actually reach the LLM’s context window. After the client receives the tool list, it doesn't dump all 50 tools into every prompt. Instead, it uses a filtering pipeline: the LLM’s initial request is analyzed, relevant tools are selected (e.g., via vector similarity on tool descriptions), and only the top-N tools are injected into the system message as JSON-serialized function definitions.&lt;/p&gt;

&lt;p&gt;In practice, a MCP client maintains an internal &lt;code&gt;ToolRegistry&lt;/code&gt; that stores all discovered tools. When the user says “show me the last 10 database queries”, the client triggers a relevance scan: it computes embeddings for the query and for each tool’s description, ranks by cosine similarity, and injects the top 5. This keeps the LLM’s context under load and avoids tool explosion. Advanced implementations even support tiered injection—critical tools (e.g., &lt;code&gt;query_database&lt;/code&gt;) are always present, while niche tools (e.g., &lt;code&gt;rotate_logs&lt;/code&gt;) are injected on demand.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Pseudocode: progressive injection logic
function injectTools(userMessage: string): Tool[] {
  const relevant = toolRegistry
    .map(t =&amp;gt; ({ tool: t, score: cosineSimilarity(embed(t.description), embed(userMessage)) }))
    .filter(x =&amp;gt; x.score &amp;gt; 0.7)
    .sort((a, b) =&amp;gt; b.score - a.score)
    .slice(0, 5);
  return relevant.map(x =&amp;gt; x.tool);
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Progressive injection also respects tool dependencies—if &lt;code&gt;query_database&lt;/code&gt; requires &lt;code&gt;auth_get_token&lt;/code&gt;, both are injected together. The MCP specification defines a &lt;code&gt;dependsOn&lt;/code&gt; array in the tool object, enabling the client to build dependency graphs and inject sets of tools atomically.&lt;/p&gt;

&lt;h2&gt;Real-World Performance: Latency Benchmarks&lt;/h2&gt;

&lt;p&gt;Testing MCP tool discovery on a server with 200 tools (average schema size 1.2KB each) yields the following metrics: handshake completes in &lt;strong&gt;12ms&lt;/strong&gt; (LAN), &lt;code&gt;tools/list&lt;/code&gt; with 20-tool pages takes &lt;strong&gt;45ms&lt;/strong&gt; total for 10 pages, and progressive injection (embedding + ranking) adds &lt;strong&gt;18ms&lt;/strong&gt;. Total time from connection to an LLM receiving relevant tools: &lt;strong&gt;~75ms&lt;/strong&gt;—far below the 200ms threshold where users perceive delay.&lt;/p&gt;

&lt;p&gt;However, when pagination is disabled (server dumps all 200 tools in a single response), payload size balloons to 240KB—adding 300ms transfer time over HTTP/1.1 and 80ms for schema parsing. This is why MCP’s design mandates cursor-based pagination and progressive injection as &lt;em&gt;recommended&lt;/em&gt; practices in the spec, not optional optimizations.&lt;/p&gt;

&lt;p&gt;Ready to build your own MCP tools with sub-100ms discovery? Dive deeper at &lt;a href="https://tormentnexus.site" rel="noopener noreferrer"&gt;TormentNexus&lt;/a&gt;—explore our open-source MCP client SDKs and interactive playground for testing tool enumeration under real traffic.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tormentnexus.site/blog/tormentnexus/mcp-protocol-deep-dive-how-tool-discovery-actually-works-under-the-hood.html" rel="noopener noreferrer"&gt;tormentnexus.site&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Beyond Synchronous Hell: Why Your Multi-Agent System Needs an Event-Driven Backbone</title>
      <dc:creator>Robert Pelloni</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:45:06 +0000</pubDate>
      <link>https://dev.to/robertpelloni/beyond-synchronous-hell-why-your-multi-agent-system-needs-an-event-driven-backbone-371n</link>
      <guid>https://dev.to/robertpelloni/beyond-synchronous-hell-why-your-multi-agent-system-needs-an-event-driven-backbone-371n</guid>
      <description>&lt;h1&gt;Beyond Synchronous Hell: Why Your Multi-Agent System Needs an Event-Driven Backbone&lt;/h1&gt;

&lt;p&gt;Explore how event-driven architecture (EDA) transforms multi-agent coordination. Learn to build a Pub/Sub backbone where Planner, Implementer, and Critic agents stay synchronized without blocking—using the Swarm event bus for async AI patterns in production.&lt;/p&gt;

&lt;h2&gt;The Synchronization Crisis in Multi-Agent Systems&lt;/h2&gt;

&lt;p&gt;Every developer who has scaled a multi-agent system beyond two agents has hit the same wall: synchronous calls create deadlocks, timeouts, and cascading failures. Imagine a Planner agent dispatching tasks to five Implementer agents while a Critic agent evaluates output in parallel. In a naive request-response system, the Planner blocks until every Implementer returns—and the Critic can't even start until the Planner finishes its orchestration loop. Latency compounds, memory pressure spikes, and a single slow agent halts the entire pipeline.&lt;/p&gt;

&lt;p&gt;In production benchmarks at TormentNexus, we observed that synchronous coordination between just three agents increased end-to-end latency by 340% compared to an event-driven equivalent. The root cause? The Planner spent 78% of its time waiting on I/O—listening for responses instead of doing actual work. This is where event-driven AI (EDA) becomes not just an optimization, but a necessity.&lt;/p&gt;

&lt;h2&gt;The Pub/Sub Pattern: Decoupling Agents with an Event Bus&lt;/h2&gt;

&lt;p&gt;Event-driven architecture inverts the control flow. Instead of one agent calling another, agents publish events onto a shared bus (the Swarm event bus) and subscribe to the events they care about. The Planner doesn't wait—it emits a "TaskAssigned" event and immediately moves on to the next task. Implementer agents pick up tasks asynchronously, and the Critic monitors a "TaskCompleted" stream without ever polling the Planner.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Example: Swarm event bus subscription for a Critic agent
const eventBus = new SwarmEventBus();

eventBus.subscribe('TaskCompleted', async (event) =&amp;gt; {
  const { taskId, implementation } = event.payload;
  const critique = await criticAgent.evaluate(implementation);
  eventBus.publish('CritiqueReady', { taskId, critique });
});

eventBus.subscribe('CritiqueReady', async (event) =&amp;gt; {
  const { taskId, critique } = event.payload;
  if (critique.score &amp;gt; 0.9) {
    eventBus.publish('TaskFinalized', { taskId });
  } else {
    eventBus.publish('TaskReassigned', { taskId, critique });
  }
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This pattern eliminates blocking. The Planner publishes tasks at a rate of 120 per minute—regardless of whether Implementers are still processing. In our tests, peak throughput jumped from 45 tasks per minute (synchronous) to 850 tasks per minute (event-driven), thanks to the decoupling effect.&lt;/p&gt;

&lt;h2&gt;How Planner, Implementer, and Critic Stay Synchronized Without Locking&lt;/h2&gt;

&lt;p&gt;Synchronization in EDA doesn't mean agents wait—it means agents converge on a shared state through events. Consider a three-agent workflow: the Planner defines a task and publishes "TaskPlanned". An Implementer consumes that event, generates code, and publishes "CodeGenerated". The Critic, subscribed to "CodeGenerated", runs validation and publishes either "CodeApproved" or "CodeRejected". The Planner subscribes to "CodeRejected" and dynamically re-plans with new constraints.&lt;/p&gt;

&lt;p&gt;This is not polling. Every agent reacts to events as they occur, not at fixed intervals. The Critic doesn't ask "Is there new code?"—it receives a push notification on the Swarm event bus. The average event propagation latency in our TormentNexus production cluster is 1.2 milliseconds, with 99.9th percentile at 8 milliseconds. That's fast enough for real-time code correction loops where the Critic must intervene before the Implementer starts the next subtask.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Event sequence for a three-agent cycle
eventBus.publish('TaskPlanned', { 
  taskId: 'task-0451', 
  spec: 'Implement OAuth2 token refresh',
  dependencies: ['auth-service'] 
});

// Implementer receives and processes asynchronously
setTimeout(() =&amp;gt; {
  eventBus.publish('CodeGenerated', { 
    taskId: 'task-0451', 
    implementation: '...code...' 
  });
}, 100); // Simulate async processing

// Critic receives and evaluates without blocking other events
eventBus.subscribe('CodeGenerated', async (evt) =&amp;gt; {
  const result = await criticAgent.validate(evt.payload.implementation);
  if (result.passed) {
    eventBus.publish('CodeApproved', { taskId: evt.payload.taskId });
  } else {
    eventBus.publish('CodeRejected', { 
      taskId: evt.payload.taskId, 
      errors: result.errors 
    });
  }
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unlike synchronous RPC, this pattern allows the Critic to batch evaluations or even spawn multiple parallel evaluation threads without the Implementer waiting. The system self-synchronizes through event causality: if event A must happen before event B, the Critic enforces that by not publishing "CodeApproved" until it has received and processed "CodeGenerated".&lt;/p&gt;

&lt;h2&gt;Handling Eventual Consistency and Conflict Resolution&lt;/h2&gt;

&lt;p&gt;Event-driven systems trade immediate consistency for availability—an essential tradeoff for async AI patterns. When an Implementer publishes "CodeGenerated" before the Critic finishes its prior evaluation, you risk processing stale state. In our architecture, every event carries a sequence number scoped to the task. The Critic maintains a FIFO queue per task and discards out-of-order events.&lt;/p&gt;

&lt;p&gt;Consider a scenario where the Planner publishes "TaskPlanned" twice due to a network retry. Without idempotency, the Implementer would produce duplicate code. The Swarm event bus handles this via event deduplication using unique event IDs (UUIDv7) and a sliding window cache. We store the last 10,000 event IDs with a TTL of 30 seconds—long enough to catch duplicates, short enough to avoid memory bloat.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Idempotent event handling in the Implementer
eventBus.subscribe('TaskPlanned', async (event) =&amp;gt; {
  const key = `processed:${event.id}`;
  if (await cache.get(key)) {
    return; // Already processed, skip
  }
  
  const implementation = await implementerAgent.code(event.payload.spec);
  await cache.set(key, true, { ttl: 30 });
  eventBus.publish('CodeGenerated', { 
    taskId: event.payload.taskId, 
    implementation 
  });
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Conflict resolution becomes critical when two agents produce competing events. For example, the Critic publishes "CodeRejected" while a parallel Reviewer agent publishes "PathologistApproved". Our rule: the more conservative event wins. If any agent signals a problem, the system treats it as blocked until a human or orchestrator resolves it. In practice, this reduced spurious approvals by 87% in our code-review pipeline over a three-month span.&lt;/p&gt;

&lt;h2&gt;Building Resilient Multi-Agent Workflows with Event Sourcing&lt;/h2&gt;

&lt;p&gt;Event sourcing complements EDA by persisting every event as the source of truth. Instead of storing the "current state" of a task (which goes stale), we store the entire event log. To know if a task is complete, we replay events: "TaskPlanned" → "CodeGenerated" → "CodeApproved" = done. This makes debugging trivial—you can trace exactly which agent published what and when.&lt;/p&gt;

&lt;p&gt;At TormentNexus, we implemented event sourcing using PostgreSQL with event store tables. Each agent's subscription is backed by a persistent consumer group that tracks the last processed event offset. If an Implementer crashes mid-task, it resumes from its checkpoint—no data loss, no redundant processing. Our recovery time for a failed agent dropped from 45 seconds (with manual restart) to 2 seconds (automatic replay).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Event store schema (simplified)
CREATE TABLE event_store (
  event_id UUID PRIMARY KEY,
  event_type VARCHAR(100) NOT NULL,
  aggregate_id VARCHAR(64) NOT NULL,  -- e.g., task-0451
  payload JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  sequence_number BIGINT NOT NULL
);

CREATE INDEX idx_aggregate ON event_store (aggregate_id, sequence_number);

-- Replay events for a task to reconstruct state
SELECT event_type, payload 
FROM event_store 
WHERE aggregate_id = 'task-0451' 
ORDER BY sequence_number;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This pattern also enables temporal queries—like "show me the state of task-0451 as of 10 minutes ago"—which are invaluable for debugging async AI patterns where agents operate at different speeds. We store 100 million events per day across our production cluster, with 99th percentile query times under 50 milliseconds.&lt;/p&gt;

&lt;p&gt;Ready to eliminate synchronous bottlenecks in your multi-agent system? Implement event-driven AI with the Swarm event bus today. &lt;a href="https://tormentnexus.site" rel="noopener noreferrer"&gt;Join the TormentNexus platform&lt;/a&gt; and build agents that communicate without blocking—start your free trial now.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tormentnexus.site/blog/tormentnexus/beyond-synchronous-hell-why-your-multi-agent-system-needs-an-event-driven-backbone.html" rel="noopener noreferrer"&gt;tormentnexus.site&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Why Vercel is still my default for shipping frontend projects</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:39:49 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/why-vercel-is-still-my-default-for-shipping-frontend-projects-2dd6</link>
      <guid>https://dev.to/swapnoneel123/why-vercel-is-still-my-default-for-shipping-frontend-projects-2dd6</guid>
      <description>&lt;p&gt;Last week, I was working on a client project with a fast approaching deadline. The work had already piled up, so I had to move really fast; I was constantly making changes, pushing them straight to GitHub, checking them through the preview link of the deployment, and going straight to the next task. And while doing so, I barely stopped and worried about hosting, because Vercel was already connected. And after successfully delivering the project within the stipulated time, it hit me that I probably could not have moved that quickly if the deployment itself had been another thing to manage.&lt;/p&gt;

&lt;p&gt;That made me realise: Vercel has been my default choice for a long time, and it is not because I am completely locked into the platform. From time to time, I still reach for other services like Cloudflare, Netlify, and Railway as well, but for my personal projects and fast development cycles, I somehow always end up coming back to Vercel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhi0hdheml3nyt0vahqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhi0hdheml3nyt0vahqv.png" alt="Easy deployment using Vercel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I mostly use Next.js, so I know the tech nerds out there will assume that, it is the entire reason why I choose Vercel, and that's a fair assumption to make, because it's partly true. Vercel develops and maintains Next.js, so of course it provides the best hosting for Next.js, but that's just one side of the coin.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I use Vercel in my projects
&lt;/h2&gt;

&lt;p&gt;If you check the projects section on &lt;a href="https://www.swapnoneel.site/work" rel="noopener noreferrer"&gt;my portfolio&lt;/a&gt;, you will find that most of the web projects I currently have are deployed through Vercel. And not all of them are Next.js applications; you will find projects with React, TanStack tooling, Node.js, and Bun as well. These are not just weekend experiments or hobby projects, either. Some of them have real users as well! Let me give you &lt;a href="https://scholarian.vercel.app" rel="noopener noreferrer"&gt;Scholarian&lt;/a&gt; as an example. It is a research platform built on Next.js, and according to my latest project analytics, it currently has more than 75 active users and over 700 chat sessions.&lt;/p&gt;

&lt;p&gt;The funny thing is that I did not think twice about deploying most of these projects. I connected the repository, gave Vercel the required environment variables, and pushed the code. That absence of thought is the whole point. But then again, I also use Cloudflare Pages and Railway for actual work, so this is not a “Vercel is perfect and everything else is bad” argument. I have reasons for coming back, but I also know where the platform starts becoming the wrong tool. So, let's discuss!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do I keep coming back to Vercel?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First of all, Vercel's preview deployment workflow!&lt;/strong&gt; It makes my development cycle much smoother. By default, every non-production branch can receive its own preview URL, and I can share that URL before merging the branch. That's extremely useful for catching visual problems before they reach production. A pull request may look completely fine during code review, but you can never know when the actual interface breaks at a particular viewport width. This has happened to me a lot. Just a few days ago, I shipped the near-final version of a project to one of my clients without noticing that, in the mobile version, a heading was overlapping one of the image assets. Preview deployments let people test the thing instead of trying to imagine it from a diff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv2i3pb2qsck36pkmg69w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv2i3pb2qsck36pkmg69w.png" alt="Two collaborators pass browser-preview cards between them; one catches a mobile layout issue before the preview reaches the production flag, illustrating Vercel preview deployments for visual QA" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a solo developer and freelancer like me, this saves a lot of time because, as you can see in the above screenshot, Vercel adds a toolbar to preview deployments where collaborators can leave comments directly on the page. This was especially useful during hackathons, when we were short on time. And our team always communicated in that way, and my teammates would drop in and leave comments like "the link to this button is redirecting to the pricing page instead of the features page" or "the color is way too contrasty." The small catch is that they need a Vercel account to comment, and external collaboration has some plan-specific limits, so it is not entirely frictionless, but still, it is much easier than sending Loom videos, annotated screenshots, or five messages explaining which button or font your client or peers want. And they have an optional third-party integration as well that can convert a preview comment into a GitHub issue. This makes conversations with my clients and non-technical collaborators much easier!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqg1urb8i88yv7xv4xp0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqg1urb8i88yv7xv4xp0.png" alt="A Vercel preview deployment with the toolbar open, showing on-page comments and collaboration controls" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Next.js experience is the other reason I keep using it.&lt;/strong&gt; Vercel develops the framework, so features such as Incremental Static Regeneration, Server Actions, React Server Components, route handlers, and streaming work with very little platform-specific configuration, and I don't have to spend an afternoon figuring out how a new Next.js feature maps onto the hosting environment. Vercel covers that part for me by default.&lt;/p&gt;

&lt;p&gt;Now, to be fair, other platforms have improved a lot, and Netlify currently supports the major Next.js features through its OpenNext adapter, including Server Components, Server Actions, streaming, ISR, and Partial Prerendering. Cloudflare can also run Next.js using its own OpenNext-based adapter. So the difference is no longer that Next.js features simply do not work elsewhere, because that would be an outdated argument. The difference is that Vercel remains the first-party deployment target, and that means there is one less compatibility layer to worry about. And this removes a pain point for me, especially when I am using a newer framework feature. And that's the edge I'm actually talking about. For a normal static React or Vite application, this advantage matters much less, but for a serious Next.js project, it becomes my go-to option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjsg16c5dflp26kwpu8a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjsg16c5dflp26kwpu8a.png" alt="Vercel Analytics dashboard displaying visitor, page-view, and bounce-rate trends for swapnoneel.site" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then there's the DX at the dashboard level. These are minute things, but together, they make a big difference for me. For example, the environment variables are scoped per environment (local, preview, and production; all of them are isolated). Rolling back to any previous deployment takes two clicks. And the deployment logs actually tell you what failed, not just that it did, and because of that, they become much easier to fix if you are taking the “pasting it into Claude Code” route.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the alternatives to Vercel?
&lt;/h2&gt;

&lt;p&gt;Well, when we are talking about the alternatives, &lt;strong&gt;Cloudflare Pages&lt;/strong&gt; is the one that comes the closest. And we know how much tech Twitter is divided on this one, and how frequently we see their representatives fight each other on open threads regarding this (I enjoy watching those heated arguments, lol). And yeah, Cloudflare is genuinely fast, and &lt;a href="https://www.cloudflare.com/network/" rel="noopener noreferrer"&gt;their edge network spans 300+ locations&lt;/a&gt;, and for static content, the performance gap over Vercel is actually quite measurable. And what I appreciate most is that the pricing is much more predictable; because, first of all, there are no egress fees, and they also provide unlimited bandwidth on the free tier. And as a bonus, I have also seen them &lt;a href="https://x.com/IanLandsman/status/2059289714264273337" rel="noopener noreferrer"&gt;helping start-ups from time to time as well&lt;/a&gt;, which is a great initiative, in my opinion.&lt;/p&gt;

&lt;p&gt;I respect all of this, but the problem is that Cloudflare's Workers environment runs on V8 isolates, which is different from a standard Node.js runtime. And this is where I face the most problems. For purely static sites or projects that have lightweight edge functions, it's totally fine, but sometimes, with specific packages that exclusively require a Node.js runtime, you start to face error messages. And although &lt;code&gt;nodejs_compat&lt;/code&gt; mode now supports a substantial portion of the Node API, the compatibility is still not perfect.&lt;/p&gt;

&lt;p&gt;There is also a trade-off in how the two platforms approach infrastructure. If you want databases, KV stores, or smart routing in your project, you must understand Cloudflare's broader ecosystem, like D1, KV, and routing rules, which is great when you want that level of control. But Vercel abstracts all of that by default. It is basically a trade-off of infrastructure control for speed, and I prefer Vercel's simpler deployment workflow in this regard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3h31y2wjulcmgfq09qf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3h31y2wjulcmgfq09qf.png" alt="A developer considers two workshop paths: a rich, intricate tool cart on one side and a smooth launch ramp carrying a website paper airplane on the other, illustrating the trade-off between infrastructure control and shipping speed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; was my original platform before I switched. I have nothing against it, honestly. It is very similar to Vercel in a lot of ways. But Vercel's integration with Next.js, which I just discussed in detail in the previous section, makes Netlify feel like it's one step behind. Features like Server Actions and React Server Components work natively on Vercel, while on Netlify, they have to go through adapters that often lag behind new framework releases, which is a big compromise. And another thing: Netlify's core CDN infrastructure also has fewer edge locations than Vercel's 100+ node network, and that's visible in the global TTFB numbers as well. I'd still use Netlify for a simple static site with a form or two because their built-in form handling is actually clever. But for a Next.js project, Vercel is my primary choice.&lt;/p&gt;

&lt;p&gt;Now, for &lt;strong&gt;Railway&lt;/strong&gt;, it's a bit different, and I use it when I need a persistent backend, like maybe a WebSocket server, a background job, or something that can't be serverless. In Scholarian, I have a long-running task where the background worker has to produce a long report using Gemini, and that process generally takes three to four minutes, so I switched the backend of my app to Railway. For Vercel, that's where it genuinely breaks down. If you need a long-running process, you're either doing something hacky with edge functions or you're reaching for a different platform. And there are a couple of good options besides Railway, and for that, &lt;a href="https://encore.dev" rel="noopener noreferrer"&gt;Encore&lt;/a&gt; would be my personal recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Vercel actually falls short
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The pricing model!&lt;/strong&gt; And that's the part I like the least. The free Hobby plan is capped, so it cannot generate an on-demand surprise bill, but if a hobby project exceeds its allowances, it may be paused or restricted instead. That's why I keep an eye out for my portfolio site, because that's the one that gets the most traffic. The bigger billing concern begins as soon as you switch to the Pro plan, where usage beyond the included credit can be charged across multiple resources. Being mindful enough is particularly important here, because I have seen a lot of posts on Reddit and X where developers have complained about the same issue.&lt;/p&gt;

&lt;p&gt;Vercel Pro currently has a 20 USD monthly platform fee, which includes one deploying seat and 20 USD of usage credit. And for additional developer seats that can deploy or configure the projects, they will cost you another 20 USD per month, but the read-only viewer seats are free. And that combination can become difficult to predict when a project grows.&lt;/p&gt;

&lt;p&gt;Vercel provides spending alerts and lets paid teams configure actions such as pausing projects after reaching a limit. Hence, it's better to enable those controls instead of assuming that traffic will always stay predictable. Sudden bot traffic, a poorly optimized function, image transformations, or a sudden spike in legitimate users can all consume usage faster than expected. So, it's always better to keep those factors in mind so that you don't get overcharged accidentally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other limitation is compute.&lt;/strong&gt; Vercel Functions can handle APIs, server-rendered routes, streaming, and other request-driven tasks, and the current function limits are far more generous. But if your application requires a continuously running background process or custom Docker containers, Vercel isn't the right fit. There are platforms like &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt; or &lt;a href="https://northflank.com" rel="noopener noreferrer"&gt;Northflank&lt;/a&gt; that are built for that kind of workload. Vercel is a frontend cloud, so the moment you need full-stack infrastructure, you're pairing Vercel with something else anyway. Hence, the title of my blog says why I prefer it for frontend projects, and not full-stack projects!&lt;/p&gt;

&lt;p&gt;And then there is also vendor lock-in, although I do not think it is as simple as people make it sound. And it's not limited to Vercel either; almost every service provider has its own kind of vendor lock-in. A static React or Vite project is easy to move, but a Next.js application that depends heavily on Vercel’s caching behavior, image optimization, routing, integrations, and deployment settings will take more effort to migrate. The more platform-specific behavior you adopt, the less portable your application becomes, and that's true for Vercel, Cloudflare, AWS, and almost every other cloud platform. I have not experienced the issue myself, but I have seen people complaining about it, so I included it in the blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the Vercel free tier actually good enough?
&lt;/h2&gt;

&lt;p&gt;For most side projects, yes! And their &lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Hobby plan&lt;/a&gt; is free for personal use and is pretty generous. It comes with unlimited projects, automatic HTTPS, custom domains, preview deployments, and 100 GB of bandwidth per month, and that's mostly enough for personal use. And I never felt the need to purchase their paid Pro plans.&lt;/p&gt;

&lt;p&gt;But the moment you add a team or need more bandwidth or function execution, you have to go to the Pro plan that starts at $20/month for each member.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the verdict?
&lt;/h2&gt;

&lt;p&gt;Vercel is still my default for frontend deployment and a part of my development cycle. The preview URL workflow alone has saved me more debugging cycles than I can count. I've tried the alternatives in actual projects, and none of them gave me back the time I was spending on deployment issues.&lt;/p&gt;

&lt;p&gt;That said, if you're cost-conscious and don't mind the learning curve, then Cloudflare is still a great choice. And if you need a backend, pick Railway and point your Vercel frontend at it. And if you've moved away from Vercel for something specific, or if you have a setup that works better for you, I'd genuinely love to hear about it in the comments!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5ibesykgl49praqtv1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5ibesykgl49praqtv1c.png" alt="Thanks for reading! What are you using to ship? Tell me in the comments." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you want to see the kinds of projects I've actually shipped on Vercel, check them out at &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;. Also, you can find me on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://github.com/Swpn0neel" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to talk or connect.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>reviews</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Commerce And Secrets Without An IAP Tax</title>
      <dc:creator>Shai Almog</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:39:12 +0000</pubDate>
      <link>https://dev.to/codenameone/commerce-and-secrets-without-an-iap-tax-1n9b</link>
      <guid>https://dev.to/codenameone/commerce-and-secrets-without-an-iap-tax-1n9b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyknc8pr9ig13patg4wkc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyknc8pr9ig13patg4wkc.jpg" alt="Commerce And Secrets Without An IAP Tax" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Commerce is the easiest feature in this release to misunderstand, so the first sentence has to be blunt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Codename One?&lt;/strong&gt; Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at &lt;a href="https://www.codenameone.com/" rel="noopener noreferrer"&gt;codenameone.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commerce does not replace IAP and never will.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Purchases still go through Apple, Google, or the payment processor you chose. Codename One does not process the payment, does not touch the money, and does not take a percentage. &lt;a href="https://github.com/codenameone/CodenameOne/pull/5300" rel="noopener noreferrer"&gt;PR #5300&lt;/a&gt; adds infrastructure around the annoying backend work that comes after a purchase: validation, entitlement checks, subscription lifecycle, webhooks, and reporting.&lt;/p&gt;

&lt;p&gt;That backend work is real. Anyone who has shipped subscriptions knows the trap. Buying a SKU is not the same as knowing whether the user has the right to a feature right now. Renewals, grace periods, refunds, billing retry, product changes, trials, family sharing and store server notifications all show up later. The device has one view. The store has another. Your backend usually needs a third.&lt;/p&gt;

&lt;p&gt;Commerce is the optional service that turns that mess into an entitlement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog539w882pvnk6iphow8.png" alt="Commerce dashboard for receipt validation, entitlements and revenue metrics" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBcHAgY2FsbHMgQ29tbWVyY2VNYW5hZ2VyLnN1YnNjcmliZSgpIl0gLS0-IEJbIlB1cmNoYXNlIEFQSSJdCiAgICBCIC0tPiBDWyJBcHBsZSAvIEdvb2dsZSBzdG9yZSBmbG93Il0KICAgIEMgLS0-IERbIlN0b3JlIHJlY2VpcHQiXQogICAgRCAtLT4gRVsiQ29tbWVyY2UgcmVmcmVzaCgpIl0KICAgIEUgLS0-IEZbIkNsb3VkIHJlY2VpcHQgdmFsaWRhdGlvbiJdCiAgICBGIC0tPiBHWyJFbnRpdGxlbWVudCBjYWNoZSJdCiAgICBHIC0tPiBIWyJpc0VudGl0bGVkKHBybykiXQogICAgQyAtLT4gU3RvcmVPS1siUHVyY2hhc2Ugc3RpbGwgY29tcGxldGVzIGV2ZW4gaWYgY2xvdWQgdmFsaWRhdGlvbiBpcyB1bmF2YWlsYWJsZSJd%3Ftype%3Dpng%26bgColor%3Dffffff" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBWyJBcHAgY2FsbHMgQ29tbWVyY2VNYW5hZ2VyLnN1YnNjcmliZSgpIl0gLS0-IEJbIlB1cmNoYXNlIEFQSSJdCiAgICBCIC0tPiBDWyJBcHBsZSAvIEdvb2dsZSBzdG9yZSBmbG93Il0KICAgIEMgLS0-IERbIlN0b3JlIHJlY2VpcHQiXQogICAgRCAtLT4gRVsiQ29tbWVyY2UgcmVmcmVzaCgpIl0KICAgIEUgLS0-IEZbIkNsb3VkIHJlY2VpcHQgdmFsaWRhdGlvbiJdCiAgICBGIC0tPiBHWyJFbnRpdGxlbWVudCBjYWNoZSJdCiAgICBHIC0tPiBIWyJpc0VudGl0bGVkKHBybykiXQogICAgQyAtLT4gU3RvcmVPS1siUHVyY2hhc2Ugc3RpbGwgY29tcGxldGVzIGV2ZW4gaWYgY2xvdWQgdmFsaWRhdGlvbiBpcyB1bmF2YWlsYWJsZSJd%3Ftype%3Dpng%26bgColor%3Dffffff" alt="Diagram" width="515" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Entitlements Instead Of SKU Branches
&lt;/h2&gt;

&lt;p&gt;Your app should not need to know every SKU that grants &lt;code&gt;pro&lt;/code&gt;. It should ask for &lt;code&gt;pro&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAppUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;unlockProFeatures&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Purchases are still delegated to the existing &lt;code&gt;Purchase&lt;/code&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purchase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remove_ads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a purchase, or when the app starts, refresh off the EDT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="no"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callSerially&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pro"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;unlockProFeatures&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}).&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;refresh()&lt;/code&gt; validates the current receipts with the cloud when the build has a &lt;code&gt;build_key&lt;/code&gt; and commerce is enabled. In a local build or simulator, it safely falls back to the normal &lt;code&gt;Purchase&lt;/code&gt; path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Quota Runs Out
&lt;/h2&gt;

&lt;p&gt;This is the real question that matters most. If Commerce is tiered, what happens when a developer exceeds quota?&lt;/p&gt;

&lt;p&gt;Validation degrades. Purchases do not stop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CommerceManager.isDegraded()&lt;/code&gt; tells you the cloud did not return a server-validated answer. In that state, entitlement checks fall back to the platform's own receipt signal, treating the entitlement id as a subscription SKU when no cached cloud answer exists. That is less rich than server-side validation, but it is the right failure mode. A paying user should not be locked out because your account hit a validated-volume cap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDegraded&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Commerce validation degraded; using store-direct fallback"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commerce is tiered because it is a backend service that can be abused: receipt validation, store API calls, lifecycle processing, webhook delivery and revenue analytics cost real infrastructure. The degradation rule is what keeps that business reality from becoming user pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Commerce Adds
&lt;/h2&gt;

&lt;p&gt;The service can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate receipts against Apple and Google.&lt;/li&gt;
&lt;li&gt;Normalize subscription state across stores.&lt;/li&gt;
&lt;li&gt;Track entitlements by your app user id.&lt;/li&gt;
&lt;li&gt;Forward lifecycle webhooks to your backend with HMAC signatures.&lt;/li&gt;
&lt;li&gt;Present revenue metrics such as MRR, ARR, ARPU, churn, trial conversion, cohort retention and realized LTV.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app-facing API remains small because the complicated part lives server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CommerceManager&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CommerceManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAppUserId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myAccountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEntitled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remove_ads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split explains how Commerce complements IAP instead of replacing it. &lt;code&gt;Purchase&lt;/code&gt; starts the transaction. Commerce answers the longer-term entitlement question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets
&lt;/h2&gt;

&lt;p&gt;The same PR adds &lt;code&gt;com.codename1.security.Secrets&lt;/code&gt;, which solves a different but related problem: API keys do not belong in source code or in the app binary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8rt1119afvi8n97844n.png" alt="Secrets dashboard for managing app-readable secrets and server-side credentials" width="800" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run off the EDT; the first call may hit the network.&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;mapsKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is fetched from the Codename One Cloud vault over TLS and cached in &lt;code&gt;SecureStorage&lt;/code&gt;. &lt;code&gt;refresh(name)&lt;/code&gt; forces a fresh fetch after you rotate the value server-side, and &lt;code&gt;clear(name)&lt;/code&gt; drops the cached copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"googlemaps.key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only app-readable secrets are served to the device. Server-only credentials, such as App Store Connect keys or Google Play service account JSON used for commerce validation, stay in the vault and are never reachable from client code.&lt;/p&gt;

&lt;p&gt;That rule is non-negotiable: do not check API keys into source, do not paste them into snippets, and do not embed server credentials in the binary. If the app can read a secret, a determined attacker can eventually extract it. Secrets reduces exposure and makes rotation easier; it does not turn a client app into a secure server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boundary
&lt;/h2&gt;

&lt;p&gt;Commerce and Secrets are both cloud features, but they sit on different sides of the volume line. Secrets usage stays low enough to enable it for everyone. Commerce has tiers because validation and analytics can create real backend load.&lt;/p&gt;

&lt;p&gt;The important boundary is not tiering. The boundary is lock-in. You can still use the raw &lt;code&gt;Purchase&lt;/code&gt; API. You can still build your own receipt backend. You can still ship an app that sells subscriptions without giving Codename One a revenue share.&lt;/p&gt;

&lt;p&gt;Commerce is there to remove backend pain, not to insert a toll booth.&lt;/p&gt;

</description>
      <category>java</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Skip LinkedIn/Indeed: most companies' job boards have a public JSON API</title>
      <dc:creator>Noble Ronin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:33:36 +0000</pubDate>
      <link>https://dev.to/ronin13/skip-linkedinindeed-most-companies-job-boards-have-a-public-json-api-48b</link>
      <guid>https://dev.to/ronin13/skip-linkedinindeed-most-companies-job-boards-have-a-public-json-api-48b</guid>
      <description>&lt;p&gt;If you've ever tried to pull job listings by scraping LinkedIn or Indeed, you know the pain: anti-bot systems, CAPTCHAs, rotating proxies, and scripts that silently break every few weeks.&lt;/p&gt;

&lt;p&gt;Here's the thing — you usually don't need any of that.&lt;/p&gt;

&lt;p&gt;Companies don't post jobs on LinkedIn first. They post them in their &lt;strong&gt;ATS&lt;/strong&gt; (Applicant Tracking System) — Greenhouse, Lever, Ashby, Workday, etc. — and most ATS platforms expose the company's board as a &lt;strong&gt;public JSON endpoint&lt;/strong&gt;. No key, no login, no browser. It's the company's own source of truth, so it's cleaner and fresher than any aggregator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoints
&lt;/h2&gt;

&lt;p&gt;A few that work with a plain GET (&lt;code&gt;{company}&lt;/code&gt; = the company's slug):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greenhouse&lt;/strong&gt; — &lt;code&gt;https://boards-api.greenhouse.io/v1/boards/{company}/jobs?content=true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lever&lt;/strong&gt; — &lt;code&gt;https://api.lever.co/v0/postings/{company}?mode=json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruitee&lt;/strong&gt; — &lt;code&gt;https://{company}.recruitee.com/api/offers/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breezy HR&lt;/strong&gt; — &lt;code&gt;https://{company}.breezy.hr/json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SmartRecruiters, Ashby, BambooHR and Personio have their own equivalents. Workday is the one annoying exception — it's a POST and needs the full board URL (tenant + datacenter + site), so you can't guess it from a bare company name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: pulling Stripe's open roles (Python)
&lt;/h2&gt;

&lt;p&gt;Stripe uses Greenhouse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;company&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stripe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://boards-api.greenhouse.io/v1/boards/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/jobs?content=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No Selenium, no proxy, no CAPTCHA solver. Runs in ~200ms and won't break next Tuesday because Cloudflare changed something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-detecting the ATS
&lt;/h2&gt;

&lt;p&gt;If you don't know which ATS a company uses, just try them in order and take the first one that returns jobs. A bare &lt;code&gt;404&lt;/code&gt; means "not this ATS, try the next." Greenhouse → Lever → Ashby → SmartRecruiters → Recruitee → Breezy covers a huge chunk of tech companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; are lenient but real — be polite, set a &lt;code&gt;User-Agent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions&lt;/strong&gt;: Greenhouse/Lever include full HTML descriptions in the list; Breezy's list endpoint doesn't (you'd fetch each posting for that).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday&lt;/strong&gt;: POST-only, needs the full board URL — worth special-casing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you don't want to build/maintain 9 of these
&lt;/h2&gt;

&lt;p&gt;I got tired of keeping a fetcher per ATS in sync, so I bundled 9 of them (Greenhouse, Lever, Ashby, SmartRecruiters, Workday, BambooHR, Personio, Recruitee, Breezy) into one thing that auto-detects the ATS from a bare company name or board URL and returns normalized jobs: &lt;strong&gt;&lt;a href="https://apify.com/ponderable_hydrometer/multi-ats-jobs" rel="noopener noreferrer"&gt;https://apify.com/ponderable_hydrometer/multi-ats-jobs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But honestly, the endpoints above are the real value — build your own if you'd rather.&lt;/p&gt;

&lt;p&gt;Which ATS should I add next? Considering Workable, Teamtailor and JazzHR.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>api</category>
      <category>jobs</category>
    </item>
    <item>
      <title>The Solana Program Security Checklist I Wish I'd Had on Day One</title>
      <dc:creator>Lymah</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:32:41 +0000</pubDate>
      <link>https://dev.to/lymah/the-solana-program-security-checklist-i-wish-id-had-on-day-one-1e8l</link>
      <guid>https://dev.to/lymah/the-solana-program-security-checklist-i-wish-id-had-on-day-one-1e8l</guid>
      <description>&lt;p&gt;I spent the last two weeks thinking like an attacker.&lt;/p&gt;

&lt;p&gt;I wrote tests whose only job was to make my own programs fail. I ran a fuzzer across thousands of generated inputs looking for the lamport value nobody would choose by hand. And I rebuilt the missing owner check that was at the center of the $326M Wormhole exploit, in a throwaway program, in a test, so I could watch it work and then watch the one-line fix stop it cold.&lt;/p&gt;

&lt;p&gt;This checklist is what I would hand to past me on day one of that work.&lt;br&gt;
Run it top to bottom before any Anchor program goes to mainnet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You are writing Solana programs in Anchor. You understand accounts, PDAs, and CPIs. You have read the Anchor docs. What you do not yet have is a systematic way to check that you have not missed the failure modes that are specific to Solana's runtime, an account model where any account can be passed into any instruction, arithmetic that wraps silently in release builds without protection, and cross-program calls that trust whatever program ID you hand them.&lt;/p&gt;

&lt;p&gt;This checklist is that systematic check. It is not a substitute for a professional audit on high-value programs. It is the thing you run before you even consider requesting one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Wormhole anchor
&lt;/h2&gt;

&lt;p&gt;Before the list, the story that explains why account validation sits at the top.&lt;/p&gt;

&lt;p&gt;In February 2022, an attacker drained $326M from the Wormhole bridge. The root cause was a single deprecated function, &lt;code&gt;load_instruction_at&lt;/code&gt;&lt;br&gt;
— that read a sysvar account's contents without first checking that the account was actually the real instructions sysvar. The attacker passed in a forged account they controlled. The program read it, trusted it, and authorized a mint it should have refused.&lt;/p&gt;

&lt;p&gt;The fix was a single word: switch to &lt;code&gt;load_instruction_at_checked&lt;/code&gt;, which verifies the account's address before reading it.&lt;/p&gt;

&lt;p&gt;Every item in this checklist traces back to that same principle:&lt;br&gt;
&lt;strong&gt;never read an account's contents until you have confirmed its&lt;br&gt;
identity.&lt;/strong&gt; The items below are just that principle applied to every surface where you might forget it.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://ackee.xyz/blog/2022-solana-hacks-explained-wormhole/" rel="noopener noreferrer"&gt;Ackee Blockchain Wormhole breakdown&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://www.halborn.com/blog/post/explained-the-wormhole-hack-february-2022" rel="noopener noreferrer"&gt;Halborn analysis&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Account Validation
&lt;/h2&gt;

&lt;p&gt;These are the checks Anchor helps most with, and the ones where&lt;br&gt;
reaching past Anchor's types creates the most risk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Every deserialized account has its owner verified.&lt;/strong&gt; Use &lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; instead of &lt;code&gt;UncheckedAccount&amp;lt;'info&amp;gt;&lt;/code&gt; or raw &lt;code&gt;AccountInfo&lt;/code&gt;. &lt;code&gt;Account&amp;lt;T&amp;gt;&lt;/code&gt; verifies the account is owned by your program and carries the right 8-byte discriminator before deserializing. A raw &lt;code&gt;AccountInfo&lt;/code&gt; does neither.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;No &lt;code&gt;UncheckedAccount&lt;/code&gt; without a &lt;code&gt;/// CHECK:&lt;/code&gt; comment that explains why skipping validation is safe.&lt;/strong&gt; Anchor forces you to write this comment, treat every occurrence as a flag for manual review. If you cannot write a convincing sentence, switch to a typed account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Account types are distinguished by their 8-byte discriminator.&lt;/strong&gt; Anchor stamps every account with a discriminator derived from the type name. &lt;code&gt;Account&amp;lt;'info, Vault&amp;gt;&lt;/code&gt; rejects a &lt;code&gt;Config&lt;/code&gt; account passed in its place. Raw deserialization has no such guard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;PDA addresses are re-derived and compared, not trusted from the caller.&lt;/strong&gt; Use &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; constraints. An attacker can create a vault-shaped account at an arbitrary address; the seeds constraint proves the address is the canonical one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;The stored bump is used, not re-derived.&lt;/strong&gt; Compute the bump once at initialization, store it on the account, and reuse it with &lt;code&gt;bump = state.bump&lt;/code&gt;. Re-deriving adds compute and is unnecessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;&lt;code&gt;remaining_accounts&lt;/code&gt; are validated before use.&lt;/strong&gt; Anchor does not automatically check accounts passed through &lt;code&gt;remaining_accounts&lt;/code&gt;. If you use them, verify the owner discriminator and signer status manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Anchor does automatically:&lt;/strong&gt; owner check, discriminator check, signer flag, reinitialization guard (with &lt;code&gt;init&lt;/code&gt;). &lt;strong&gt;What it does not:&lt;/strong&gt; business logic, arithmetic, CPI target identity, or anything involving &lt;code&gt;remaining_accounts&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Authority and Signer Checks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Every privileged instruction confirms the expected signer.&lt;/strong&gt; &lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; verifies the account signed the transaction.&lt;code&gt;has_one = authority&lt;/code&gt; verifies the stored key matches. You need both: &lt;code&gt;has_one&lt;/code&gt; without &lt;code&gt;Signer&lt;/code&gt; lets anyone who knows the authority pubkey (which is public) call the instruction without their consent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;No instruction trusts a pubkey without also checking its&lt;br&gt;
signer flag.&lt;/strong&gt; Knowing a public key is not the same as being able&lt;br&gt;
to sign for it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Admin or upgrade authority accounts are explicitly constrained.&lt;/strong&gt; Use &lt;code&gt;#[account(address = EXPECTED_PUBKEY)]&lt;/code&gt; for hardcoded admins, not a manual pubkey comparison buried in handler logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;&lt;code&gt;has_one&lt;/code&gt; relationships are complete.&lt;/strong&gt; If &lt;code&gt;vault.authority&lt;/code&gt;must equal &lt;code&gt;authority.key()&lt;/code&gt;, the &lt;code&gt;has_one = authority&lt;/code&gt; constraint says so declaratively and fails before your handler runs. A manual&lt;code&gt;require_keys_eq!&lt;/code&gt; in the handler is a second line of defense, not the first.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Arithmetic Safety
&lt;/h2&gt;

&lt;p&gt;I fuzzed a &lt;code&gt;deposit&lt;/code&gt; function with a property test that ran hundreds of generated &lt;code&gt;u64&lt;/code&gt; pairs. The property: a deposit either grows the balance or returns &lt;code&gt;None&lt;/code&gt; honestly. With raw &lt;code&gt;+&lt;/code&gt;, the property fails on overflow inputs, the balance wraps to a tiny number, and the test catches it. With &lt;code&gt;checked_add&lt;/code&gt;, every overflow returns &lt;code&gt;None&lt;/code&gt; and the test passes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Every balance or supply change uses &lt;code&gt;checked_add&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;checked_sub&lt;/code&gt;, &lt;code&gt;checked_mul&lt;/code&gt;, or &lt;code&gt;checked_div&lt;/code&gt;.&lt;/strong&gt; Never raw &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt; on untrusted values. Solana's runtime sets &lt;code&gt;overflow-checks = true&lt;/code&gt; for release builds, which turns arithmetic overflow into a panic rather than a silent wrap — but &lt;code&gt;checked_*&lt;/code&gt; lets you return a named error instead of a panic, which is cleaner for users and easier to test.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Overflow and underflow are handled explicitly, not suppressed.&lt;/strong&gt; &lt;code&gt;.ok_or(VaultError::InsufficientFunds)?&lt;/code&gt; is correct.&lt;code&gt;.unwrap_or(u64::MAX)&lt;/code&gt; is not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;No silent cast that could truncate.&lt;/strong&gt; A &lt;code&gt;u64 → u32&lt;/code&gt; cast&lt;br&gt;
silently drops the upper bits. Use &lt;code&gt;u64::try_from(value)?&lt;/code&gt; and&lt;br&gt;
handle the error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Division by zero is guarded.&lt;/strong&gt; If a denominator can be&lt;br&gt;
user-supplied, check it before dividing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. CPI Safety
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Every CPI verifies the target program ID is the expected one.&lt;/strong&gt; Do not accept the program ID from the caller without checking it. Use &lt;code&gt;Program&amp;lt;'info, Token&amp;gt;&lt;/code&gt; or &lt;code&gt;Interface&amp;lt;'info, TokenInterface&amp;gt;&lt;/code&gt; rather than passing a raw &lt;code&gt;AccountInfo&lt;/code&gt; for the program.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Accounts are reloaded after a CPI if their data is read again.&lt;/strong&gt; A CPI can modify account state. Anchor does not automatically refresh your local view. Reload the account before reading fields that a CPI may have changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;PDA signer seeds are correct and complete.&lt;/strong&gt; When a PDA signs a CPI with &lt;code&gt;.with_signer(signer_seeds)&lt;/code&gt;, the seeds must include every component you used at initialization, including the bump byte. A mismatch causes a silent authorization failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;CPIs from a PDA do not propagate the wrong authority.&lt;/strong&gt; The PDA signs on its own terms. If your CPI passes additional signers, confirm each one is intentional.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Account Lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Closed accounts cannot be revived within the same transaction.&lt;/strong&gt; Closing an account in instruction N and reopening it in instruction N+1 of the same transaction can reuse the zeroed data. Use Anchor's &lt;code&gt;close = destination&lt;/code&gt; constraint, which zeroes the data, transfers lamports, and marks the account in a way that prevents reuse within the transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;No instruction allows reinitializing an initialized account.&lt;/strong&gt; Use &lt;code&gt;init&lt;/code&gt; (not &lt;code&gt;init_if_needed&lt;/code&gt;) when you mean "create exactly once." &lt;code&gt;init_if_needed&lt;/code&gt; silently does nothing if the account already exists, which can mask a caller reusing an account they do not own. If you must use &lt;code&gt;init_if_needed&lt;/code&gt;, add an explicit check that the stored authority matches the current signer before trusting the existing data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Rent-exempt deposits are accounted for.&lt;/strong&gt; Accounts must maintain a minimum lamport balance. If your program moves lamports, confirm the source account remains rent-exempt after the transfer.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Pre-Deploy Hygiene
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;&lt;code&gt;anchor keys sync&lt;/code&gt; has been run and the declared ID matches the keypair.&lt;/strong&gt; A program deployed with a mismatched ID will fail every transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Dependencies are current and free of known advisories.&lt;/strong&gt; Run &lt;code&gt;cargo audit&lt;/code&gt; before deploying. A single vulnerable dependency can compromise an otherwise correct program.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Adversarial tests pass, not just the happy path.&lt;/strong&gt; If your test suite only verifies that valid inputs succeed, it proves nothing about what invalid inputs do. You need at least one test per security-critical constraint that proves the constraint actually fires when violated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;Property tests cover arithmetic functions.&lt;/strong&gt; If you have a function that modifies a balance, a property test with a fuzzer (proptest, Trident) will find inputs your examples missed. A hand-picked example list cannot cover all of &lt;code&gt;u64&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] &lt;strong&gt;The program has been reviewed by someone who did not write it.&lt;/strong&gt; Familiarity blindness is real. A reviewer who reads the code cold will ask questions the author stopped asking weeks ago.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Anchor helps and where it does not
&lt;/h2&gt;

&lt;p&gt;Anchor's typed accounts automatically handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Program ownership verification (&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Discriminator check (rejects the wrong account type)&lt;/li&gt;
&lt;li&gt;Signer flag verification (&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reinitialization guard (&lt;code&gt;init&lt;/code&gt; constraint)&lt;/li&gt;
&lt;li&gt;PDA address verification (&lt;code&gt;seeds&lt;/code&gt; + &lt;code&gt;bump&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anchor cannot help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your business logic (who is allowed to call what, under what
conditions)&lt;/li&gt;
&lt;li&gt;Arithmetic correctness (checked vs unchecked operations)&lt;/li&gt;
&lt;li&gt;CPI target identity (you must verify the program ID yourself)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remaining_accounts&lt;/code&gt; (no automatic validation)&lt;/li&gt;
&lt;li&gt;Anything you expressed as an &lt;code&gt;UncheckedAccount&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checklist exists because the second list is where every real&lt;br&gt;
exploit lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to use this checklist
&lt;/h2&gt;

&lt;p&gt;Print it or keep it open in a browser tab. Before any mainnet deploy, go through it top to bottom. For each item, answer yes or no against the actual code, not the code you remember writing. The items are written to be verifiable line by line.&lt;/p&gt;

&lt;p&gt;This is a living document. If you find a gap, open an issue or leave a comment. A security checklist that never gets updated is just technical debt with a nice format.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.anchor-lang.com/docs/account-constraints" rel="noopener noreferrer"&gt;Anchor account constraints reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ackee.xyz/blog/2022-solana-hacks-explained-wormhole/" rel="noopener noreferrer"&gt;Ackee Blockchain: Wormhole breakdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.halborn.com/blog/post/explained-the-wormhole-hack-february-2022" rel="noopener noreferrer"&gt;Halborn: Wormhole hack explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/coral-xyz/sealevel-attacks" rel="noopener noreferrer"&gt;Sealevel attacks catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.soteria.dev/" rel="noopener noreferrer"&gt;Soteria audit framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.neodyme.io/posts/solana_common_pitfalls/" rel="noopener noreferrer"&gt;Neodyme: Solana common pitfalls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built from bugs I reproduced myself during Arc 12 of&lt;br&gt;
*&lt;/em&gt;#100DaysOfSolana*&lt;em&gt;. If something is wrong, tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>security</category>
      <category>rust</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>Building an Agentic FinOps Platform — Development Environment Setup, Google Antigravity, MCPs and Skills, and ADK Bootstrapping with Agents CLI</title>
      <dc:creator>Darren "Dazbo" Lester</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:31:20 +0000</pubDate>
      <link>https://dev.to/gde/building-an-agentic-finops-platform-development-environment-setup-google-antigravity-mcps-and-4c43</link>
      <guid>https://dev.to/gde/building-an-agentic-finops-platform-development-environment-setup-google-antigravity-mcps-and-4c43</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6st9qzdipor9ezy6ama.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6st9qzdipor9ezy6ama.gif" alt="Boostrapping with Agents-CLI and Skills" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR — This article is going to be jam-packed with useful information, tips, tricks and hacks for setting up an agentic development in the Google ecosystem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one isn’t really about the FinOps!&lt;/p&gt;

&lt;h2&gt;
  
  
  Welcome to Part 2
&lt;/h2&gt;

&lt;p&gt;Welcome back, friends!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46lbn528ktckl4sspjsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46lbn528ktckl4sspjsl.png" alt="Dr Evil — Welcome Back!" width="498" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/google-cloud/finsavant-part-1-building-an-agentic-finops-platform-with-google-adk-a2ui-and-gemini-enterprise-248f59cea3a0"&gt;first part&lt;/a&gt;, I described the purpose of the &lt;a href="https://github.com/derailed-dash/smart-gcp-finops" rel="noopener noreferrer"&gt;FinSavant&lt;/a&gt; FinOps solution, the motivation for creating it, its overall architecture and tech stack, and how it works.&lt;/p&gt;

&lt;p&gt;In this part, we’ll use &lt;em&gt;FinSavant&lt;/em&gt; as a case study in &lt;strong&gt;how to set up a development environment&lt;/strong&gt; for the purposes of building such an ADK-based agentic solution. &lt;strong&gt;Even if you’re not particularly interested in &lt;em&gt;FinSavant&lt;/em&gt; itself, I hope you’ll find a bunch of useful information and tips here that will help you build your own agentic solutions more effectively and quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Using Antigravity IDE&lt;/li&gt;
&lt;li&gt; Overall project workspace structure&lt;/li&gt;
&lt;li&gt; Setting up agent skills for your coding agent&lt;/li&gt;
&lt;li&gt; My project’s &lt;code&gt;GEMINI.md&lt;/code&gt; (or if you prefer, &lt;code&gt;AGENTS.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt; My documentation approach&lt;/li&gt;
&lt;li&gt; Setting up MCP servers for your coding agent, such as BigQuery MCP&lt;/li&gt;
&lt;li&gt; Scaffolding the initial ADK agent using Google Agents CLI and its supporting skill&lt;/li&gt;
&lt;li&gt; Getting started with a &lt;code&gt;Makefile&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sound good? Let’s get cracking!&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Orientation
&lt;/h2&gt;

&lt;p&gt;Let’s see where we are in this series.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;a href="https://dev.to/gde/finsavant-part-1-building-an-agentic-finops-platform-with-google-adk-a2ui-and-gemini-enterprise-29l3"&gt;Goals, Architecture, and Tech Stack: Capabilities, project goals, target architecture, technology stack, and design decisions.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt; Development Environment Setup, Google Antigravity, MCPs and Skills, and ADK Bootstrapping with Agents CLI &lt;strong&gt;📍 You are here.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; Building the ADK Agent and API&lt;/li&gt;
&lt;li&gt; Designing and Building the UI with Google Stitch and A2UI&lt;/li&gt;
&lt;li&gt; Deployment with Gemini Enterprise Agent Platform, Agent Runtime, Cloud Run and IAP&lt;/li&gt;
&lt;li&gt; Automating Deployment with CI/CD and Terraform&lt;/li&gt;
&lt;li&gt; Agent Observability, Evaluation, and Tuning with Gemini Enterprise Agent Platform&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started with Antigravity IDE
&lt;/h2&gt;

&lt;p&gt;These days, my favourite coding environment &lt;em&gt;for any significant project&lt;/em&gt; is Antigravity IDE. This is Google’s &lt;em&gt;agent-first&lt;/em&gt; integrated development environment. You get a look-and-feel that’s familiar to VS Code users, but powered with autonomous, context-aware agents that can plan, execute, verify, and work in parallel.&lt;/p&gt;

&lt;p&gt;You can get it &lt;a href="https://antigravity.google/product/antigravity-ide?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By the way, Antigravity IDE is just one member of the Antigravity (aka Agy) suite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8lmru48uhh4sc2x3zf0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8lmru48uhh4sc2x3zf0.jpeg" alt="Google Antigravity Suite" width="700" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve covered these before, but here’s a quick reminder of the four Agy solutions in the suite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Antigravity 2.0&lt;/strong&gt;, which is now the dedicated agent-first “builder” environment on your desktop. Notably, it doesn’t itself include an IDE. Instead, we now interact only with the agent manager. This surface aims to usher in the era of “idea to product” using agents, without concerning ourselves over the code. Many builders who don’t come from a coding background will love this.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Antigravity IDE&lt;/strong&gt;, which gives us the more familiar VS Code-esque coding environment, supported by the Antigravity agent harness. Here we can do agent-assisted development, and we always see the code. Coders will feel at home here.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Antigravity SDK&lt;/strong&gt;, which gives you the harness and tools that power Antigravity, but exposed as a Python Agent SDK. By importing from &lt;code&gt;google.antigravity&lt;/code&gt; we can programmatically leverage Antigravity’s capabilities.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Antigravity CLI&lt;/strong&gt;, which is the next evolution of the extremely awesome Gemini CLI. It’s still a terminal-first environment for interacting with Gemini models. But the new Antigravity CLI is built in Go, and you can tell; it feels much faster than Gemini CLI, both during startup and in general use. It leverages the same agent “harness” as Antigravity 2.0 and the IDE, and this allows for common settings and configuration across the Antigravity suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Here’s the rough outline of the project structure we’ll be creating. We won’t be building all of this structure here; nor does this represent the final state of the project. But it gives you an idea of where we’re heading. (I’ll explain the &lt;code&gt;*&lt;/code&gt; in a minute!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  smart-gcp-finops/  
  ├── agent/                # ADK agent package  
  │   ├── finops_agent/     # Root agent  
  │   ├── .env              # Agent specific environment vars  
  │   ├── Dockerfile        # For deploying agent to Agent Runtime  
  │   └── pyproject.toml    # Agent runtime dependencies  
* ├── bff/                # Backend-for-Frontend (API)  
* ├── deployment/         # Infrastructure &amp;amp; CI/CD (Terraform IaC)  
* │   ├── terraform/      # Centralised IaC for Prod &amp;amp; Staging  
  │   └── README.md         # Deployment documentation  
* ├── docs/               # Project documentation  
* │   ├── images/         # Diagrams and architectural visual assets  
  │   ├── DESIGN.md         # Visual identity, components, and UI design  
  │   ├── architecture-and-walkthrough.md # Solution blueprints, ADRs, and component data flows  
  │   └── testing.md        # Testing strategy and verification instructions  
* ├── frontend/           # React UI frontend  
* ├── notebooks/          # Jupyter notebooks for prototyping and evaluation  
* ├── scripts/            # Environment setup and other utility scripts  
  │   └── setup-env.sh      # Configure local environment including Google auth / ADC  
* ├── tests/              # Unit and integration test suites  
* │   ├── eval/           # Agent evaluation  
* │   ├── unit/           # Unit tests  
* │   └── integration/    # Integration tests  
* ├── .agents/            # Workspace customizations root  
  │   └── mcp_config.json   # E.g. MCP servers  
* ├── .github/            # GitHub Actions workflows and CI/CD  
* ├── .env                # Root environment vars (dev setup, unified container, GitHub, etc)  
  ├── .envrc                # Automatically launch when entering this directory  
* ├── .gitignore          # Exclude from git  
* ├── Makefile            # Centralised developer convenience commands  
* ├── GEMINI.md           # Development agent context &amp;amp; guidelines  
* ├── LICENSE             # Standard open-source license file  
* ├── pyproject.toml      # Root project configuration / dependencies  
* ├── README.md           # Developer documentation homepage  
  └── TODO.md               # TODO list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wanted to build such a structure from scratch, here’s a cool thing to try…&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create your new project folder, e.g. &lt;code&gt;my-cool-project&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Open that folder in Antigravity IDE.&lt;/li&gt;
&lt;li&gt; Supply this prompt to the Agy Agent:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/grill-me Using this folder tree as a template, 
create the required folder structure in this workspace for my new Python 
project. Only create folders and files that are marked as '*'. 
For required files, provide initial starter-for-10 content. 
&amp;lt;&amp;lt; paste the tree structure here &amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;/grill-me&lt;/code&gt;? This is a built-in Agy command that causes the agent to ask questions to remove ambiguity. If you were to give the agent a slightly vague prompt without this prefix, then the agent might make some guesses about what you want. But with &lt;code&gt;/grill-me&lt;/code&gt;, the agent will still make educated guesses, but it will also ask you questions to clarify your intent.&lt;/p&gt;

&lt;p&gt;The prompt above is a good example of where this is useful. You’ll notice that my project tree has a &lt;code&gt;LICENSE.md&lt;/code&gt; file, which is a standard component to include in open-source projects. But my prompt doesn't specify which license to use. So when you use &lt;code&gt;/grill-me&lt;/code&gt;, the agent will offer sensible license choices based on your project and context, and ask you to confirm.&lt;/p&gt;

&lt;p&gt;This video demonstrates Agy scaffolding the entire project from scratch, in response to the prompt above:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/DmnBHilRjOo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Give it a go!&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills for Your Coding Agent
&lt;/h2&gt;

&lt;p&gt;I like to describe skills as &lt;strong&gt;units of knowledge that agents load on-demand&lt;/strong&gt;, when they need to do a particular task. I’ve previously written articles on the subject of my favourite skills, where to find them, and how to install them. I recommend you check out &lt;a href="https://dev.to/google-cloud/dialling-our-agents-to-11-agent-skills-you-need-to-be-using-ccffa51e91df"&gt;this one&lt;/a&gt;. You might want to go ahead and install all of my favourites!&lt;/p&gt;

&lt;p&gt;But for now, let’s add a few skills that will definitely be useful for our current project. I recommend installing them globally, so they’ll be available to all of your development projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/vercel-labs/skills &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;--skill&lt;/span&gt; find-skills  
npx skills add https://github.com/derailed-dash/dazbo-agent-skills &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;  
npx skills add https://github.com/google/skills &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;  
npx skills add https://github.com/google-gemini/gemini-skills/ &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;  
npx skills add https://github.com/shubhamsaboo/awesome-llm-apps/awesome-agent-skills &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;--skill&lt;/span&gt; technical-writer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re also going to install the &lt;em&gt;Google Agents CLI&lt;/em&gt; and its associated skills, but we’ll get to that later.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;GEMINI.md&lt;/code&gt; - Context for Your Coding Agent
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;GEMINI.md&lt;/code&gt; file (or &lt;code&gt;AGENTS.md&lt;/code&gt; if you prefer) is how you define your project's rules and context. It's where you tell the Agy Agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  About your project’s goals&lt;/li&gt;
&lt;li&gt;  Rules and guidelines you want it to follow&lt;/li&gt;
&lt;li&gt;  References you want it to read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we create &lt;code&gt;GEMINI.md&lt;/code&gt; in the root of a project then the file is scoped only to &lt;em&gt;that project&lt;/em&gt;. (This project-specific context gets appended to any global &lt;code&gt;GEMINI.md&lt;/code&gt; you have defined.) When you launch any Antigravity tool from this workspace - such as Agy 2.0, Agy IDE, or Agy CLI - the Agent will automatically read this context.&lt;/p&gt;

&lt;p&gt;Let me show you what my &lt;code&gt;GEMINI.md&lt;/code&gt; looked like, when starting out with &lt;em&gt;FinSavant&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# FinSavant - the Agentic FinOps Solution  &lt;/span&gt;

&lt;span class="gu"&gt;## Project Goals  &lt;/span&gt;

To create an agentic FinOps solution for GCP that:  
&lt;span class="p"&gt;-&lt;/span&gt; Uses ADK for agent orchestration.  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to examine billing and cost data in BigQuery, based on billing   
  exports.  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to understand Google Cloud infrastructure and services across   
  multiple Google projects associated with a billing account.  
&lt;span class="p"&gt;-&lt;/span&gt; Considers projects associated with a particular Google Cloud organisation,    
  associated with a billing account.  
&lt;span class="p"&gt;-&lt;/span&gt; Leverages Google Developer Knowledge API MCP for grounding:   
  Google APIs, Google Cloud infrastructure, Google Cloud best practices.  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to detect cost anomalies and inefficiencies, and trends.  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to understand all deployed infra and services, and historical   
  configuration changes, leveraging Google Cloud Asset Inventory  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to invoke Google Cloud Assist for immediate logs investigation,   
  RCA and recommendations.  
&lt;span class="p"&gt;-&lt;/span&gt; Is able to combine all of the above to provide actionable insights and   
  recommendations to users.  
&lt;span class="p"&gt;-&lt;/span&gt; Provides a UI for users, which includes:  
&lt;span class="p"&gt;  -&lt;/span&gt; Dashboard of cost trends, billing data and anomalies  
&lt;span class="p"&gt;  -&lt;/span&gt; Cost forecasting  
&lt;span class="p"&gt;  -&lt;/span&gt; Cost analysis  
&lt;span class="p"&gt;  -&lt;/span&gt; Anomaly detection  
&lt;span class="p"&gt;  -&lt;/span&gt; Recommendations  
&lt;span class="p"&gt;  -&lt;/span&gt; Cost optimisation suggestions  
&lt;span class="p"&gt;  -&lt;/span&gt; A natural language chat interface  
&lt;span class="p"&gt;-&lt;/span&gt; The UI should be based on React. Use skills you have available to leverage   
  React best practices.  
&lt;span class="p"&gt;-&lt;/span&gt; Leverage Google Stitch to design the UI, and use the Stitch MCP server to   
  pull in the design, in order to convert to React.  
&lt;span class="p"&gt;-&lt;/span&gt; The UI is connected to the agent via FastAPI.  
&lt;span class="p"&gt;-&lt;/span&gt; The UI and API will be hosted in a single Cloud Run service. The service   
  will be secured using IAP, using direct Cloud Run integration - no   
  Load Balancer.  
&lt;span class="p"&gt;-&lt;/span&gt; The Agent will be deployed to Agent Runtime in Gemini Enterprise Agent   
  Platform.  

&lt;span class="gu"&gt;## Tool Use: Skills, Gemini Enterprise Agent Platform, Agent Runtime and ADK  &lt;/span&gt;

Be sure to use all &lt;span class="gs"&gt;**agents**&lt;/span&gt; skills, &lt;span class="gs"&gt;**Gemini Enterprise Agent Platform**&lt;/span&gt;   
skills, and &lt;span class="gs"&gt;**ADK**&lt;/span&gt; skills you have available for developing ADK agents and   
best practices, and use &lt;span class="gs"&gt;**adk-docs-mcp**&lt;/span&gt; for latest ADK documentation.   

You will have additional skills available to you, but always check if the   
following can help with a particular task.  

&lt;span class="gu"&gt;### ADK &amp;amp; agents-cli Lifecycle Skills  &lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`google-agents-cli-workflow`&lt;/span&gt;: Entrypoint for building ADK agents (scaffold,   
build, evaluate, deploy, publish, observe).  
[skipping for brevity]  

&lt;span class="gu"&gt;### Gemini Enterprise Agent Platform APIs  &lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`gemini-api`&lt;/span&gt;: Gemini Enterprise Agent Platform, Google Cloud, and   
Agent Platform enterprise usage with the Google Gen AI SDK.  
[skipping for brevity]  

&lt;span class="gu"&gt;### Agent Platform Engine &amp;amp; Model Management  &lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`agent-platform-deploy`&lt;/span&gt;: Deploying models and tuned weights to Agent   
  Platform endpoints.  
[skipping for brevity]  

&lt;span class="gu"&gt;## Key Internal Documentation  &lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; README.md - Project README; the developer's front door  
&lt;span class="p"&gt;-&lt;/span&gt; TODO.md - High level plan for the project  
&lt;span class="p"&gt;-&lt;/span&gt; architecture-and-walkthrough.md - The main architecture, including design   
  decisions  
&lt;span class="p"&gt;-&lt;/span&gt; DESIGN.md - Where we will capture the UI design  
&lt;span class="p"&gt;-&lt;/span&gt; testing.md - Where we will document test strategy, summary of tests,   
  testing instructions, any manual testing processes  
&lt;span class="p"&gt;-&lt;/span&gt; docs/blog.md - A blog post document we will build along the way  
&lt;span class="p"&gt;-&lt;/span&gt; /deployment/README.md - Deployment and CI/CD documentation  

&lt;span class="gu"&gt;## Essential Reading  &lt;/span&gt;

You should read and leverage these resources for guidance and best practices,   
in addition to the skills and MCP servers you have available for knowledge.  

| Resource | Description and Relevance |  
| -------- | ------------------------- |  
| https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco | Use the BigQuery MCP server |   
| https://adk.dev/integrations/bigquery/?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco | BigQuery tool for ADK |  
| https://docs.cloud.google.com/gemini-enterprise-agent-platform?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco | Gemini Enterprise Agent Platform Overview |  
| https://adk.dev/deploy/agent-runtime?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco | ADK with Agent Runtime |  
[skipping for brevity]  

&lt;span class="gu"&gt;## Other Notes  &lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; "Vertex AI" no longer exists as a product; the replacement is Gemini   
  Enterprise Agent Platform.  
&lt;span class="p"&gt;-&lt;/span&gt; "Vertex AI Agent Engine" is no more; the replacement is "Agent Runtime",   
  which is a part of the Gemini Enterprise Agent Platform.  
&lt;span class="p"&gt;-&lt;/span&gt; But APIs and Google internal resource names may still refer to legacy names,   
  e.g. &lt;span class="sb"&gt;`reasoningEngine`&lt;/span&gt; rather than Agent Runtime. Always use the new names   
  when creating documentation, but be mindful that we may need to use old   
  names in API calls and certain resource definitions.  

&lt;span class="gu"&gt;## Blog  &lt;/span&gt;

I want to build a multi-part blog series, which I'll post on Medium and Dev.to.  

&lt;span class="gu"&gt;### Documenting As We Go  &lt;/span&gt;

As we go, document steps taken, experience and findings in docs/blog.md.   
Later, I will build a Medium blog from this content. During this "as we go"   
phase, the blog.md does not need to be a collection of notes, code snippets, and observations. It should:  
&lt;span class="p"&gt;
-&lt;/span&gt; Include all the key steps we did, in the order we did them.  
[skipping for brevity]  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re following along and you’ve just created your context file, give Agy a restart now, so it picks this up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation Approach
&lt;/h2&gt;

&lt;p&gt;I’m a big fan of having a consistent set of high-quality, continuously maintained documentation. I even have my own agent skill — &lt;code&gt;maintaining-core-documentation&lt;/code&gt; - to automate much of this for me. Check out my previous blog on this subject: &lt;a href="https://dev.to/google-cloud/documentation-as-context-a-skill-to-automate-your-blueprints-for-the-agentic-era-2bec0cf041a3"&gt;Documentation as Context: A Skill to Automate Your Blueprints for the Agentic Era&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you previously ran the &lt;code&gt;npx skills add https://github.com/derailed-dash/dazbo-agent-skills -y -g&lt;/code&gt; command from above, then you already have this skill installed.&lt;/p&gt;

&lt;p&gt;With this in place, you could issue this prompt to bootstrap a set of documentation for a brand-new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use maintaining-core-documentation to bootstrap my project documentation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out this video to see the skill doing its magic!&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fvT_GJ4LPhE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;As you evolve your project, this skill will automatically maintain your documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Servers for Your Coding Agent
&lt;/h2&gt;

&lt;p&gt;I’ve previously written about &lt;a href="https://dev.to/google-cloud/dialling-our-agents-to-11-my-favourite-mcp-servers-9549c1442a5e"&gt;some of my favourite MCP servers&lt;/a&gt;. There are only a couple that we’ll need for this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Google BigQuery Remote MCP Server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://adk.dev/tutorials/coding-with-ai/?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco#adk-docs-mcp-server" rel="noopener noreferrer"&gt;ADK Docs MCP&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that we won’t be using either of these in the &lt;em&gt;FinSavant&lt;/em&gt; agent itself. These are purely to help us during development.&lt;/p&gt;

&lt;p&gt;Let’s try out the BigQuery MCP server first. In your workspace’s &lt;code&gt;.agents/mcp_config.json&lt;/code&gt; file, we configure the &lt;strong&gt;Remote BigQuery MCP server&lt;/strong&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"bigquery-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://bigquery.googleapis.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="nl"&gt;"authProviderType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google_credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="nl"&gt;"oauth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;  
          &lt;/span&gt;&lt;span class="s2"&gt;"https://www.googleapis.com/auth/bigquery"&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="nl"&gt;"x-goog-user-project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-gcp-billing-project"&lt;/span&gt;&lt;span class="w"&gt;  
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note about this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Billing project&lt;/strong&gt;: Replace &lt;code&gt;your-gcp-billing-project&lt;/code&gt; with the Google project where your billing data export lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo7b4crvrdlpd4ef8qn9l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo7b4crvrdlpd4ef8qn9l.png" alt="BigQuery MCP Configuration" width="700" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;BigQuery API&lt;/strong&gt;: Make sure the BigQuery API (&lt;code&gt;bigquery.googleapis.com&lt;/code&gt;) is enabled on that project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Developer Identity Permissions&lt;/strong&gt;: Because the MCP server uses &lt;code&gt;google_credentials&lt;/code&gt; to authenticate, your local developer account (active in &lt;code&gt;gcloud auth&lt;/code&gt;) must be authorised on Google Cloud. You need the &lt;code&gt;roles/bigquery.dataViewer&lt;/code&gt; and &lt;code&gt;roles/bigquery.jobUser&lt;/code&gt; roles on the project hosting the billing dataset.&lt;/li&gt;
&lt;li&gt;  You also need the &lt;code&gt;roles/mcp.toolUser&lt;/code&gt; role, in order to use this managed MCP server to query the BigQuery database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now, when you open the workspace in Antigravity IDE, it will load this configuration automatically. Your coding agent will be able to query schemas, inspect tables, and try out SQL queries in order to assist you when you actually create the &lt;em&gt;FinSavant&lt;/em&gt; agent code.&lt;/p&gt;

&lt;p&gt;Let’s test it!&lt;/p&gt;

&lt;p&gt;First, I issue this prompt to the Agy agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What billing tables do I have? Explain their key functions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu1hy9rer1ya227wazpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu1hy9rer1ya227wazpf.png" alt="What billing tables do I have?" width="700" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the agent immediately finds the MCP server and asks for permission to invoke its tools. After I grant permission, I get this response:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqp17qkpwtbsp1vno4fy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqp17qkpwtbsp1vno4fy.png" alt="BQ MCP response" width="629" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nice! You can see how helpful this is going to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffolding Your ADK Agent
&lt;/h2&gt;

&lt;p&gt;The easiest way to scaffold a new ADK agent is to make use of &lt;strong&gt;Google Agents CLI&lt;/strong&gt;. The Agents CLI is actually a bundle, containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The &lt;strong&gt;Agents CLI&lt;/strong&gt; itself — commands for scaffolding, evaluating, deploying, and observing AI agents on Google Cloud. The &lt;a href="https://github.com/google/agents-cli" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; describes the commands available:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyy9tqvje0ncsab949yg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyy9tqvje0ncsab949yg.png" alt="Agents-CLI commands" width="700" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  An associated set of &lt;strong&gt;agent skills&lt;/strong&gt; that turn your development agent into an expert in using Agents CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylr6ixtlvs7jyxg8dkn2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylr6ixtlvs7jyxg8dkn2.png" alt="Agents-CLI skills" width="700" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You install the bundle using this one-time command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx google-agents-cli setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already have it installed, then you can upgrade like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool upgrade google-agents-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(It’s worth doing this occasionally — this CLI is evolving quickly!)&lt;/p&gt;

&lt;p&gt;With this installed, we &lt;em&gt;could&lt;/em&gt; create our top level &lt;code&gt;agent&lt;/code&gt; folder that contains a root agent called &lt;code&gt;finops_agent&lt;/code&gt; by running this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agents-cli scaffold create agent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--agent&lt;/span&gt; adk &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prototype&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--agent-directory&lt;/span&gt; finops_agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foukl5oi8onyaezg0dbeo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foukl5oi8onyaezg0dbeo.png" alt="Running agents-cli scaffold create" width="615" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll end up with the following inside of your workspace folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent/  
├── finops_agent/                 # Your agent code  
│   ├── __init__.py               # Registers the app (exports `app`)  
│   ├── agent.py                  # Agent definition — instructions, model, tools  
│   └── app_utils/                # Utilities (telemetry, converters)  
│       ├── __init__.py  
│       ├── telemetry.py          # OpenTelemetry setup for Cloud Trace  
│       ├── typing.py             # Request/response Pydantic models  
│       └── gcs.py                # GCS utility functions  
│  
├── tests/  
│   ├── eval/                     # Evaluation test cases  
│   │   ├── datasets/  
│   │   │   └── basic-dataset.json    # Default eval cases  
│   │   └── eval_config.yaml          # Evaluation metrics configuration  
│   ├── integration/  
│   │   └── test_agent.py         # Integration test (runs agent end-to-end)  
│   └── unit/  
│       └── test_dummy.py         # Placeholder for unit tests  
│  
├── .env                          # Environment variables (project ID, location)  
├── .env.example                  # Example environment variables  
├── .gitignore                    # Git ignore file  
├── pyproject.toml                # Project config and dependencies  
├── agents-cli-manifest.yaml      # Configuration for agents-cli  
├── Dockerfile                    # Dockerfile for the agent runtime  
└── GEMINI.md                     # Guidance file for coding agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But since we now have the skills installed, there’s an easier way to accomplish this, that doesn’t require you to check the CLI documentation…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please bootstrap a new ADK agent project. The agent top-level project should   
be named `agent`, and it should contain a root `agent-directory` called   
`finops_agent`, NOT the default of `app`. This means `pyproject.toml` and   
other config files will live under `agent/`, and all Python source files   
(like `agent.py` and `fast_api_app.py`) will live inside   
`agent/finops_agent/`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sure, this prompt is quite detailed, but I’m after a very specific folder structure.&lt;/p&gt;

&lt;p&gt;Let’s see a live demo…&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/wxMK7MJwHqA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;As you can see from the demo, we can now use &lt;code&gt;agents-cli&lt;/code&gt; to check if our newly scaffolded agent is working.&lt;/p&gt;

&lt;p&gt;For example, by issuing a single test prompt on the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;agent  
agents-cli run &lt;span class="s2"&gt;"Hello! Who are you?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj97qru6g5fku1iwxf2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj97qru6g5fku1iwxf2j.png" alt="agents-cli run" width="700" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or, we could run up the extremely powerful and useful &lt;strong&gt;ADK Web&lt;/strong&gt; interface, using this handy shortcut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From the agent folder&lt;/span&gt;
agents-cli playground
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdg0eav3679kw1v7nwpr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdg0eav3679kw1v7nwpr.png" alt="agents-cli playground" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concluding thought about &lt;code&gt;Agents-CLI&lt;/code&gt;: if you know your way around the CLI, you can use it directly. It'll be faster and use fewer tokens. But when you're doing lots of agent related activities like boostrapping, adding CI/CD, deploying and evaluating, you'll probably find that natural language conversations are going to save you a lot of time and pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus #1: Getting Started with a Makefile
&lt;/h2&gt;

&lt;p&gt;In a “monorepo” project setup like &lt;em&gt;FinSavant&lt;/em&gt;, you quickly end up managing a lot of moving parts: building frontend assets, compiling Python environments, building and running multiple Docker images, executing test suites, and deploying resources to various target environments.&lt;/p&gt;

&lt;p&gt;Rather than forcing yourself (or your team) to remember a massive list of commands and flags, wrapping them in a &lt;code&gt;Makefile&lt;/code&gt; is a big win.&lt;/p&gt;

&lt;p&gt;But what actually is a &lt;code&gt;Makefile&lt;/code&gt;? At its core, it is a configuration file used by the classic &lt;code&gt;make&lt;/code&gt; build automation tool. These days it has evolved into a lightweight, standardised task runner. It allows us to define short aliases (known as "targets") for complex shell commands, documenting project workflows in a single, standard file that is easy for both humans and agents to discover.&lt;/p&gt;

&lt;p&gt;Earlier, during the demonstration of bootstrapping the project, the agent actually created an initial &lt;code&gt;Makefile&lt;/code&gt; for us. If you followed along, you'll now have a &lt;code&gt;Makefile&lt;/code&gt; that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;install lint format lint-fix test  &lt;/span&gt;

&lt;span class="nl"&gt;install&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; uv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uv is not installed. Installing uv..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/0.11.16/install.sh | sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin/env&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;  
    uv &lt;span class="nb"&gt;sync&lt;/span&gt;  

&lt;span class="nl"&gt;lint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
    uvx codespell@latest &lt;span class="nt"&gt;-s&lt;/span&gt;  
    uvx ruff@latest check &lt;span class="nb"&gt;.&lt;/span&gt;  

&lt;span class="nl"&gt;lint-fix&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
    uvx codespell@latest &lt;span class="nt"&gt;-w&lt;/span&gt;  
    uvx ruff@latest check &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;  

&lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
    uv run pytest tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run any of these &lt;code&gt;make&lt;/code&gt; &lt;em&gt;targets&lt;/em&gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, we can build on it in future articles as we develop the &lt;em&gt;FinSavant&lt;/em&gt; solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus #2: My Setup-Env Script
&lt;/h2&gt;

&lt;p&gt;These days, whenever I’m working on a project that makes use of Google services, I always create a helper &lt;code&gt;setup-env&lt;/code&gt; script to configure my environment for me.&lt;/p&gt;

&lt;p&gt;This script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Loads environment variables&lt;/strong&gt;: Automatically exports all key/value pairs from &lt;code&gt;.env&lt;/code&gt; directly into the current shell session.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Handles Google Cloud authentication&lt;/strong&gt;: If not skipped (via the &lt;code&gt;--noauth&lt;/code&gt; flag), it runs &lt;code&gt;gcloud auth login --update-adc&lt;/code&gt; to authenticate the user and configure Google Application Default Credentials (ADC).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Sets the active gcloud project&lt;/strong&gt;: Configures &lt;code&gt;gcloud&lt;/code&gt; defaults for the target project and billing quota project settings.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Extracts project metadata&lt;/strong&gt;: Dynamically retrieves the Google Cloud Project Number and constructs helper variables (like the Cloud Build service account email) for deployment scripts.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Synchronises Python dependencies&lt;/strong&gt;: Runs &lt;code&gt;uv sync&lt;/code&gt; to ensure all standard, development, and notebook dependencies are installed in the local environment.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Activates the virtual environment&lt;/strong&gt;: Activates the local Python virtual environment (&lt;code&gt;.venv&lt;/code&gt;) so the user is immediately ready to run code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can find a copy of this &lt;code&gt;scripts/setup-env.sh&lt;/code&gt; in my &lt;a href="https://github.com/derailed-dash/smart-gcp-finops/blob/main/scripts/setup-env.sh" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. Because it uses standard environment variables defined in your &lt;code&gt;.env&lt;/code&gt;, you can use it in any of your Google projects!&lt;/p&gt;

&lt;p&gt;You run it from the project root directory like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;scripts/setup-env.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus #3: Automating Setup with &lt;code&gt;direnv&lt;/code&gt; and &lt;code&gt;.envrc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you haven’t come across this before, I think you’re going to like it!&lt;/p&gt;

&lt;p&gt;Manually sourcing the &lt;code&gt;setup-env.sh&lt;/code&gt; script every time I open a terminal in the project directory is a bit of a chore. To automate this, we can use &lt;code&gt;direnv&lt;/code&gt; — an extension for your shell that automatically runs custom scripts and loads / unloads environment variables depending on your current directory.&lt;/p&gt;

&lt;p&gt;By placing a &lt;code&gt;.envrc&lt;/code&gt; file at the root of the project, &lt;code&gt;direnv&lt;/code&gt; automatically executes it whenever you &lt;code&gt;cd&lt;/code&gt; into the directory.&lt;/p&gt;

&lt;p&gt;Here is what our &lt;code&gt;.envrc&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;".venv"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;uv venv  
&lt;span class="k"&gt;fi&lt;/span&gt;  

&lt;span class="c"&gt;# Check if gcloud token is still valid to avoid re-authenticating  &lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;gcloud auth print-access-token &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"gcloud token is valid, skipping authentication."&lt;/span&gt;  
  &lt;span class="nb"&gt;source &lt;/span&gt;scripts/setup-env.sh &lt;span class="nt"&gt;--noauth&lt;/span&gt;  
&lt;span class="k"&gt;else  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"gcloud token is not valid, re-authenticating."&lt;/span&gt;  
  &lt;span class="nb"&gt;source &lt;/span&gt;scripts/setup-env.sh  
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration does a few smart things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Bootstraps the virtual environment&lt;/strong&gt;: Automatically initialises a virtual environment using &lt;code&gt;uv venv&lt;/code&gt; if it doesn't already exist.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Verifies active Google session&lt;/strong&gt;: Runs &lt;code&gt;gcloud auth print-access-token&lt;/code&gt; silently to check if our Google Cloud session is active.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Conditionally sources configuration&lt;/strong&gt;: If the Google Cloud session is still valid, it sources the setup-env script with the &lt;code&gt;--noauth&lt;/code&gt; flag, avoiding repetitive and annoying browser login prompts. If the session has expired, it triggers the full setup script to re-authenticate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are a couple of one-off steps we have to do to get &lt;code&gt;direnv&lt;/code&gt; up and running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Install &lt;code&gt;direnv&lt;/code&gt;. On Debian/Ubuntu systems, this is &lt;code&gt;sudo apt install direnv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Allow &lt;em&gt;this&lt;/em&gt; folder for &lt;code&gt;direnv&lt;/code&gt;. Run &lt;code&gt;direnv allow&lt;/code&gt; in the terminal, in the project folder where we've placed our &lt;code&gt;.envrc&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, when we enter our project folder, the script runs automatically, like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8muwcqhjvvadijl55akm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8muwcqhjvvadijl55akm.gif" alt="direnv demo" width="719" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty neat, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-Up and Next Steps
&lt;/h2&gt;

&lt;p&gt;Okay, we’re done with the environment setup. We’ve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Setup Google Antigravity, along with some killer skills and MCP servers&lt;/li&gt;
&lt;li&gt;  Bootstrapped our project using the Agy agent&lt;/li&gt;
&lt;li&gt;  Played with &lt;code&gt;/grill-me&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Established an initial set of core project documentation, using a custom skill&lt;/li&gt;
&lt;li&gt;  Used the Agy agent to bootstrap an ADK agent, making use of &lt;code&gt;agents-cli&lt;/code&gt; and its skills&lt;/li&gt;
&lt;li&gt;  Created a &lt;code&gt;Makefile&lt;/code&gt; for standardising common development, testing and deployment tasks&lt;/li&gt;
&lt;li&gt;  Created a &lt;code&gt;scripts/setup-env.sh&lt;/code&gt; script for setting up our Google Cloud environment&lt;/li&gt;
&lt;li&gt;  Used &lt;code&gt;direnv&lt;/code&gt; and a &lt;code&gt;.envrc&lt;/code&gt; file to automate the setup process, every time we open a terminal in this directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next part, we’ll look at the actual code for our &lt;em&gt;FinSavant&lt;/em&gt; agents and tools!&lt;/p&gt;

&lt;p&gt;See you there!&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Go
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Please share&lt;/strong&gt; this with anyone that you think will be interested. It might help them, and it really helps me!&lt;/li&gt;
&lt;li&gt;  Please &lt;strong&gt;give me loads of claps&lt;/strong&gt;! (Just hold down the clap button.)&lt;/li&gt;
&lt;li&gt;  Please &lt;strong&gt;leave a comment&lt;/strong&gt; 💬. Interaction is good!&lt;/li&gt;
&lt;li&gt;  and &lt;strong&gt;subscribe,&lt;/strong&gt; so you don’t miss my content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Useful Links and References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Project Demo &amp;amp; Portfolio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/derailed-dash/smart-gcp-finops" rel="noopener noreferrer"&gt;FinSavant on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dazbo.co.uk" rel="noopener noreferrer"&gt;Dazbo’s Portfolio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gemini Enterprise Agent Platform &amp;amp; ADK
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/overview?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Gemini Enterprise Agent Platform Overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/adk?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;ADK Agent Building Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/google/agents-cli?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Agents CLI on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://google.github.io/agents-cli/?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Agents CLI Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud Services &amp;amp; APIs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://docs.cloud.google.com/cloud-assist/overview?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Google Cloud Assist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.cloud.google.com/asset-inventory/docs/overview?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Cloud Asset Inventory (CAI) API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://developers.google.com/knowledge/mcp?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Developer Knowledge MCP Server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other Related Articles &amp;amp; Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://antigravity.google/product/antigravity-ide?utm_campaign=DEVECO_GDEMembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Antigravity IDE Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://makefiletutorial.com/" rel="noopener noreferrer"&gt;Makefile Tutorial&lt;/a&gt; — A modern, visual guide to writing GNU Makefiles.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://astral.sh/uv" rel="noopener noreferrer"&gt;uv Package Manager&lt;/a&gt; — Fast Python package manager and resolver.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.astral.sh/ruff/" rel="noopener noreferrer"&gt;Ruff Linter &amp;amp; Formatter&lt;/a&gt; — Blazing fast linter and formatter for Python.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>finops</category>
      <category>antigravity</category>
      <category>agentscli</category>
      <category>adk</category>
    </item>
    <item>
      <title>I open-sourced AEGIS: a self-hosted, flow-first personal AI orchestration platform</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:26:15 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/i-open-sourced-aegis-a-self-hosted-flow-first-personal-ai-orchestration-platform-4c74</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/i-open-sourced-aegis-a-self-hosted-flow-first-personal-ai-orchestration-platform-4c74</guid>
      <description>&lt;p&gt;For the past year I've run most of my day on a system I built for exactly one user: me. Last week I open-sourced it. It's called AEGIS, it's MIT-licensed, and this is the honest tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet
&lt;/h2&gt;

&lt;p&gt;Every week there's a new agent framework that promises to do everything. AEGIS is a smaller, stranger bet: that software can learn the shape of one person's life well enough to interrupt &lt;em&gt;less&lt;/em&gt;. It watches the boring things — tasks, email, money, a&lt;br&gt;
  knowledge base, homelab alerts — and only reaches for me when a decision is genuinely mine to make. It's not a chatbot I log into; it's a fleet of scheduled and event-driven workflows that mostly run without me.&lt;/p&gt;

&lt;p&gt;## The shape&lt;/p&gt;

&lt;p&gt;Four named agents, each a permission boundary with a personality: Sebas (GTD), Raphael (research), Maou (money), Pandora's Actor (infrastructure). The spine is FastAPI + Postgres (with pgvector) + Temporal, on a small Docker Swarm at home. Models&lt;br&gt;
  resolve through a LiteLLM proxy — local-first, reaching for Claude or GPT only when a job needs the horsepower.&lt;/p&gt;

&lt;p&gt;A few design decisions did most of the work.&lt;/p&gt;

&lt;p&gt;## One primitive for every interruption&lt;/p&gt;

&lt;p&gt;The decision I'm proudest of is a table. Every time the system needs a human, it's the same shape: a row in a Postgres &lt;code&gt;interactions&lt;/code&gt; table, a card in my chat app, and a Temporal workflow that durably waits — for days if it has to — until I tap a&lt;br&gt;
  button.&lt;/p&gt;

&lt;p&gt;Approvals, choices, drafts to review, plain acknowledgements — one mechanism, five card kinds, one callback format. No per-feature approval tables; adding a new "ask the human" moment costs nothing. And because interrupting me is now a formal act with a&lt;br&gt;
  paper trail, flows get written to do more work before they ask. That one decision turned AEGIS from a notification machine into a queue of interruptions that have to earn their way in.&lt;/p&gt;

&lt;p&gt;## Durability instead of cron-and-hope&lt;/p&gt;

&lt;p&gt;A card a workflow waits on for three days is miserable to build with cron and a queue — you hand-roll a state machine and reconcile it after every deploy. Temporal's durable execution is exactly this: the workflow awaits a signal, and the wait survives&lt;br&gt;
  restarts, redeploys, and the occasional node reboot. Schedules reconcile from DB config, so changing a flow's cadence needs no redeploy.&lt;/p&gt;

&lt;p&gt;## Behavior is data, not code&lt;/p&gt;

&lt;p&gt;The change that made AEGIS forkable was deleting every line that said &lt;code&gt;if agent == "sebas"&lt;/code&gt;. Capabilities, tool grants, and routing now live in database metadata, edited from an admin panel. The code asks "who owns GTD?" and gets an answer; it never&lt;br&gt;
  names names. Rename the agents, re-scope them, or add your own — no Python.&lt;/p&gt;

&lt;p&gt;## Local-LLM-first, for real&lt;/p&gt;

&lt;p&gt;Everything routes through a LiteLLM proxy exposing three tiers — fast / balanced / smart. Each agent is assigned a &lt;em&gt;tier&lt;/em&gt;, never a model name, so swapping models is proxy config and the app code never changes. One reasoning-model gotcha is handled&lt;br&gt;
  explicitly: reasoning models bill hidden reasoning tokens against &lt;code&gt;max_tokens&lt;/code&gt; before any visible output, so a tight cap returns &lt;code&gt;finish_reason=length&lt;/code&gt; with empty content — the client detects that and raises a typed truncation error instead of handing&lt;br&gt;
  an empty string to &lt;code&gt;json.loads&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;## What it is not&lt;/p&gt;

&lt;p&gt;Not a SaaS — no hosted version, and it does nothing until you point it at your own accounts and models. Not another framework to build on; it's a complete, opinionated application you fork and configure for your own life. If that sounds like more setup&lt;br&gt;
  than you want, reading the code is a perfectly good outcome.&lt;/p&gt;

&lt;p&gt;## Take it apart&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/hikmahtech/aegis" rel="noopener noreferrer"&gt;https://github.com/hikmahtech/aegis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The longer tour + design essays: &lt;a href="https://hikmahtechnologies.com/aegis" rel="noopener noreferrer"&gt;https://hikmahtechnologies.com/aegis&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd genuinely like to know what breaks — and what you'd reach for on the "smart" tier these days. That's the slot I still escalate most often.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>selfhosted</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Meet Krokq: a light, adaptable task tracker and chat under one shell</title>
      <dc:creator>Dmitry Isaenko</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:23:04 +0000</pubDate>
      <link>https://dev.to/d_isaenko_dev/meet-krokq-a-light-adaptable-task-tracker-and-chat-under-one-shell-adf</link>
      <guid>https://dev.to/d_isaenko_dev/meet-krokq-a-light-adaptable-task-tracker-and-chat-under-one-shell-adf</guid>
      <description>&lt;h2&gt;
  
  
  The pain this grew out of
&lt;/h2&gt;

&lt;p&gt;I help one client with blog support, and their team was working across four different messengers at once. Some people in one, some in another, part of them on a phone only. There was no single place to track anything. Every subproject landed in one shared chat, and it turned into a mess: tasks got lost, agreements were forgotten, and results were written down nowhere.&lt;/p&gt;

&lt;p&gt;The big systems are supposed to fix this. But Odoo, monday, ClickUp and the heavy ERPs are too much for a small team, packed with features nobody there will ever touch. By the time you finish setting one up, any wish to actually use it is gone.&lt;/p&gt;

&lt;p&gt;I wanted something light that fit this exact problem. That is how Krokq started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Krokq is
&lt;/h2&gt;

&lt;p&gt;Krokq ([krok] means 'step') is a light and completely free task tracker and chat under one shell.&lt;/p&gt;

&lt;p&gt;The chat is built to feel like the messengers people already use, WhatsApp, Telegram, Viber. That matters more than it sounds: nobody has to learn a new tool, it is familiar from the first minute, and the barrier to entry is almost zero. It takes the parts that actually get used: messaging, drafts, comments, voice notes, drag and drop files, reactions. But the main thing is moving freely between the two modes, Chat and Tasks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn a chat message into a Task in one click.&lt;/li&gt;
&lt;li&gt;Jump from a Task straight into a private chat with that person.&lt;/li&gt;
&lt;li&gt;Or keep the discussion right inside the Task, whichever fits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Tasks side
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A real workflow: New, In progress, Rework, In review, Done, plus Trash and Archive.&lt;/li&gt;
&lt;li&gt;Custom task types to match how a team works.&lt;/li&gt;
&lt;li&gt;A board with columns, with urgent items standing out.&lt;/li&gt;
&lt;li&gt;Threaded comments under a task, with reactions, editing and deleting.&lt;/li&gt;
&lt;li&gt;Attachments on a task, uploaded or pulled from a connected source.&lt;/li&gt;
&lt;li&gt;Search across tasks.&lt;/li&gt;
&lt;li&gt;Status based permissions: an assignee goes read only once a task is accepted or archived, for example.&lt;/li&gt;
&lt;li&gt;Assigning an owner, accepting work, or sending it back for rework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Chat side
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Private conversations with colleagues.&lt;/li&gt;
&lt;li&gt;Drafts, editing, deleting and forwarding messages.&lt;/li&gt;
&lt;li&gt;Reactions on messages.&lt;/li&gt;
&lt;li&gt;Voice messages with a waveform.&lt;/li&gt;
&lt;li&gt;Attachments: drag files into a thread and back out, paste an image from the clipboard.&lt;/li&gt;
&lt;li&gt;Presence indicators and read receipts.&lt;/li&gt;
&lt;li&gt;Quoting and replying to a message.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adaptable through connections
&lt;/h2&gt;

&lt;p&gt;This is the part I am most happy with, and the reason Krokq stays light without being limited. Krokq connects to the services a team already uses, and their content shows up right inside the app as a built in source.&lt;/p&gt;

&lt;p&gt;Two connections are ready so far, both to WordPress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gallery: browse and search a connected site's media library, read only, through the app backend.&lt;/li&gt;
&lt;li&gt;Posts: the list of the site's posts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything goes through the backend over the WP REST API and Application Passwords, you can connect several sites at once, and access is set per company. The useful bit: right from there you can build a Task or drop a message into Chat, without exporting or copying anything by hand.&lt;/p&gt;

&lt;p&gt;Under the hood each connection is a small descriptor class. Here is the WordPress Gallery one, trimmed to the essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A Krokq connection is just a small descriptor. This one pulls a&lt;/span&gt;
&lt;span class="c1"&gt;// WordPress media library into the app, read-only, through the backend.&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WpGalleryPlugin&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractPlugin&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'wp-gallery'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'WordPress Gallery'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;multiInstance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// connect several sites&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a new one for another service is the same kind of descriptor, so the list keeps growing around real requests. That is what adaptable means here: the tool shapes itself to the work, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  An app, not just a website
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Installs as an app through Chrome on Android and Safari on iOS, and on desktop too. If the system allows standalone, Krokq offers to install itself.&lt;/li&gt;
&lt;li&gt;Web Push notifications, with an unread counter right on the icon.&lt;/li&gt;
&lt;li&gt;Share files into the app: the system lists Krokq among the share targets for most file types.&lt;/li&gt;
&lt;li&gt;10 languages, more as they are needed.&lt;/li&gt;
&lt;li&gt;No mobile app yet, but the whole base for it is in place: an API plus Sanctum. So it is a matter of time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sign in and demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sign in with a login and password or through Google OAuth. Signing in on another device by QR code is next on my list.&lt;/li&gt;
&lt;li&gt;Right after signing in you can fill the app with demo data in one click (the vertical is retail, in any of the 10 languages). The demo lives for 24 hours and then deletes itself, so the sandbox is always clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this runs on
&lt;/h2&gt;

&lt;p&gt;Krokq runs on my own engine package, LaraFoundry, a core for spinning up SaaS projects fast: companies, users, roles, authentication. Krokq is the third application on that engine, and the Krokq side itself took me about three days. The engine already runs in production on other apps, and I keep polishing it in parallel.&lt;/p&gt;

&lt;p&gt;The domain is covered by tests (Pest and PHPUnit), with the usual CI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;Bigger things in progress: bulk actions on messages and attachments, data export, an admin layer for oversight, and more service connections. I will show them as they land.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow along
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://krokq.com" rel="noopener noreferrer"&gt;Krokq: krokq.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dmitryisaenko/krokq_public" rel="noopener noreferrer"&gt;Public repo: github.com/dmitryisaenko/krokq_public&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a light tracker plus chat is something your small team keeps improvising in group messengers, give Krokq a try and tell me what is missing.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>buildinpublic</category>
      <category>sass</category>
    </item>
    <item>
      <title>GitOps for Geospatial Data: Building a Self-Healing, Zero-Cost Data Pipeline with GitHub Actions</title>
      <dc:creator>Manideep Chittineni</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:21:37 +0000</pubDate>
      <link>https://dev.to/mchittineni/gitops-for-geospatial-data-building-a-self-healing-zero-cost-data-pipeline-with-github-actions-5490</link>
      <guid>https://dev.to/mchittineni/gitops-for-geospatial-data-building-a-self-healing-zero-cost-data-pipeline-with-github-actions-5490</guid>
      <description>&lt;p&gt;Most data engineering and geospatial projects follow a predictable infrastructure blueprint: an ingestion cron job, an enterprise database (like PostGIS), a hosted API layer, a dynamic tile server, and a frontend client. Before you write a single line of business logic, your cloud architectural diagram already carries a fixed monthly overhead.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;&lt;a href="https://github.com/mchittineni/india-village-finder" rel="noopener noreferrer"&gt;Village Finder&lt;/a&gt;&lt;/strong&gt; an open-source interactive mapping application tracking over 78,000 Indian villages, live market rates, and land records I have decided to reject the traditional stack.&lt;/p&gt;

&lt;p&gt;Instead, I applied core &lt;strong&gt;DevOps, GitOps, and DataOps principles&lt;/strong&gt; to build an entirely serverless, self-updating data platform. The operational infrastructure bill? &lt;strong&gt;Exactly $0.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the architectural blueprint of how I shifted left on geospatial data infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ 1. Infrastructure as Code (IaC) is Good. Data as Code is Better.
&lt;/h2&gt;

&lt;p&gt;The foundational principle of GitOps is that &lt;strong&gt;Git is the single source of truth.&lt;/strong&gt; If your infrastructure states can live in a declarative YAML file, your application data can too.&lt;/p&gt;

&lt;p&gt;In Village Finder, the core dataset of record (the structural relationship between Districts -&amp;gt; Mandals -&amp;gt; Villages) is treated exactly like code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Upstream API] ➡️ [Daily Ingestion Run] ➡️ [Automated Code Generation] ➡️ [PR Matrix]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When changes occur upstream in the Government of India’s Local Government Directory, a daily scheduled GitHub Actions pipeline handles the processing. But instead of updating an active runtime database, it generates normalized, flat-file static JSON and CSV tables and writes them &lt;strong&gt;directly back into a version-controlled Git Pull Request.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auditable Log Streams:&lt;/strong&gt; The repository’s commit history doubles as an exact, immutable audit trail of physical data mutations over time (e.g., catching exactly when local authorities reclassified or shifted thousands of village codes overnight).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Downtime rollbacks:&lt;/strong&gt; If an upstream data feed introduces corrupt schema transformations, rolling back production to a prior valid data state is as simple as a &lt;code&gt;git revert&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔀 2. Separating State: PR Pipelines vs. Historyless Data Branches
&lt;/h2&gt;

&lt;p&gt;If you commit highly volatile, real-time data metrics (like daily market prices or weekly welfare shifts) directly to your master branch, your primary git tree history will explode in size within months.&lt;/p&gt;

&lt;p&gt;To solve this, we implemented a decoupled lifecycle strategy separating &lt;strong&gt;Structural Data&lt;/strong&gt; from &lt;strong&gt;Volatile Metrics&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Data Pipeline (The PR Path)
&lt;/h3&gt;

&lt;p&gt;Changes to core geographic boundaries trigger a standard code-review loop. An automated workflow creates a Pull Request accompanied by a dynamically generated markdown changelog detailing exactly which entries were modified. Consecutive nightly runs update the &lt;em&gt;same&lt;/em&gt; open PR branch, eliminating bot spam.&lt;/p&gt;

&lt;h3&gt;
  
  
  Volatile Metrics Pipeline (The Flat Data Branch CDN)
&lt;/h3&gt;

&lt;p&gt;Daily commodity prices change constantly. For these, our automated runners bypass the Pull Request loop entirely and push directly to standalone, isolated git tracking branches (e.g., &lt;code&gt;data/mandi-prices&lt;/code&gt;) using an history-less force-push contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compile volatile snapshots into a completely history-less single-commit branch&lt;/span&gt;
&lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp &lt;/span&gt;out/&lt;span class="k"&gt;*&lt;/span&gt;.json &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git init &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; data/mandi-prices
git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"data: snapshot &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%RZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git push &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="s2"&gt;"https://x-access-token:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@github.com/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.git"&lt;/span&gt; data/mandi-prices

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub’s global static asset delivery network (&lt;code&gt;raw.githubusercontent.com&lt;/code&gt;) serves public files with open CORS permissions out-of-the-box. The browser frontend handles fetches directly from the head of these flat data branches at runtime. We get a high-availability, zero-latency data delivery network with &lt;strong&gt;no active server management.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 3. Shifting Left on Data Validation (DataOps Testing)
&lt;/h2&gt;

&lt;p&gt;In a traditional DevOps loop, Continuous Integration (CI) validates code compilation and unit tests. In a DataOps loop, CI must validate &lt;strong&gt;data integrity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single missing sub-district mapping or a broken polygon geometry will crash a client-side map canvas. To prevent this, our pipeline treats raw data updates with the same rigor as code changes. Every automated Pull Request must pass an exhaustive suite of over 90 validation rules orchestrated via &lt;code&gt;pytest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_referential_integrity&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Assert that every single village maps cleanly to a valid structural parent code
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;missing_parents&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_geojson_geometries&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Enforce polygon boundary validity before deploying static vector tile coordinates
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;geometries_are_valid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the upstream government registry outputs an incomplete schema variant, the automated testing matrix fails loud, blocks the integration path, and prevents the deployment runner from shipping broken maps into production.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☔ 4. Designing for Fragile Upstreams: The &lt;code&gt;EX_TEMPFAIL&lt;/code&gt; Contract
&lt;/h2&gt;

&lt;p&gt;Public data networks encounter high rates of transient downtime, network timeouts, and sudden firewall shifts. If your continuous deployment platform fires a critical failure alarm every single time a remote service faces a routine network drop, your team experiences &lt;strong&gt;alert fatigue&lt;/strong&gt;. You begin to ignore notifications, and true application bugs slip past unnoticed.&lt;/p&gt;

&lt;p&gt;To build an infrastructure that tolerates flaky external gateways, we adopted the classic Unix &lt;code&gt;EX_TEMPFAIL&lt;/code&gt; status contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Handle flaky connections inside ingestion layers
&lt;/span&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transient upstream network drop: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# EX_TEMPFAIL
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When our data acquisition routines encounter a terminal connection timeout after multiple retries, they explicitly exit with a status code of &lt;strong&gt;75&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Our orchestration runner identifies code &lt;code&gt;75&lt;/code&gt; not as a breaking error, but as a clean &lt;strong&gt;skip request&lt;/strong&gt;. The workflow execution concludes gracefully, retains the previous day's working data cache, logs a brief summary update, and keeps the overall build pipeline completely green. Alarms only sound if something is truly broken inside our internal code logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  📐 5. Progressive Build Aggregations (Branch Overlays)
&lt;/h2&gt;

&lt;p&gt;To keep the platform highly decoupled, we use a modular architecture where separate workflows generate independent data layers (like complex cadastral survey boundaries or village point catalogs) entirely in parallel.&lt;/p&gt;

&lt;p&gt;At deployment time, instead of running heavy multi-hour calculation steps sequentially, a highly optimized compilation worker overlays these pre-rendered data blocks straight into the target production directory using shallow checkouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Overlay Pre-Rendered Vector Boundaries&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;git fetch -q --depth 1 origin "data/boundary-tiles"&lt;/span&gt;
    &lt;span class="s"&gt;git checkout -q FETCH_HEAD -- ./tiles/boundaries.pmtiles&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a developer forks the project locally to build a minor feature patch, absent data branches skip missing files quietly. This design ensures the entire local system remains lightweight, instantly reproducible, and highly modular for open-source contributors.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Big Takeaway
&lt;/h2&gt;

&lt;p&gt;DevOps is frequently treated as an enterprise enterprise cost center—a tool setup meant for managing Kubernetes nodes or provisioning massive cloud databases.&lt;/p&gt;

&lt;p&gt;Village Finder proves that by applying core DevOps mental models—immutability, continuous data validation gates, decoupled architecture, and treating external inputs as a version-controlled pipeline—you can deliver fast public software with exceptional durability and zero maintenance costs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Live App:&lt;/strong&gt; &lt;a href="https://mchittineni.github.io/india-village-finder/" rel="noopener noreferrer"&gt;mchittineni.github.io/india-village-finder&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Explore the Actions &amp;amp; Code:&lt;/strong&gt; &lt;a href="https://github.com/mchittineni/india-village-finder" rel="noopener noreferrer"&gt;github.com/mchittineni/india-village-finder&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>devops</category>
      <category>github</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
